import java.util.*;

// 栈
class StackSol {
    // 20.有效的括号
    // 给定一个只包括 '('，')'，'{'，'}'，'['，']' 的字符串 s ，判断字符串是否有效。
    // 有效字符串需满足：
    // 左括号必须用相同类型的右括号闭合。
    // 左括号必须以正确的顺序闭合。
    // 提示：
    // 1 <= s.length <= 104
    // s 仅由括号 '()[]{}' 组成

    // 方法一：栈-时间复杂度：O(n)，空间复杂度：O(n)
    public boolean isValid(String s) {
        int n = s.length();
        if (n % 2 == 1)
            return false;

        Map<Character, Character> pairs = new HashMap<Character, Character>() {
            {
                put(')', '(');
                put(']', '[');
                put('}', '{');
            }
        };
        Deque<Character> stack = new LinkedList<Character>();
        for (int i = 0; i < n; i++) {
            char ch = s.charAt(i);
            if (pairs.containsKey(ch)) {
                if (stack.isEmpty() || stack.peek() != pairs.get(ch))
                    return false;

                stack.pop();
            } else {
                stack.push(ch);
            }
        }
        return stack.isEmpty();
    }

    // 32.最长有效括号
    // 给你一个只包含 '(' 和 ')' 的字符串，找出最长有效（格式正确且连续）括号子串的长度。

    // 方法一：栈-时间复杂度：O(n)，空间复杂度O(n)
    // 栈存储下标，栈内可能存在的情况（精髓）：1.一个右括号（表示匹配失败），2.0-n个左括号
    public int longestValidParentheses(String s) {
        int maxans = 0;
        // 栈存储下标，栈内可能存在的情况：1.有且仅有一个右括号（表示匹配失败），2.0-n个左括号
        Deque<Integer> stack = new LinkedList<Integer>();
        stack.push(-1);// 假定的右括号，作为计算长度的左边界
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(')// 左括号直接入栈
                stack.push(i);
            else {// 右括号，弹出栈顶元素
                stack.pop();
                if (stack.isEmpty())// 上一步出栈的是初始-1或唯一的一个右括号
                    stack.push(i);// 将当前右括号入栈，作为左边界
                else// 出栈的一定是左括号，此时栈内一定非空
                    maxans = Math.max(maxans, i - stack.peek());
            }
        }
        return maxans;
    }

    // 方法二：双指针 + 双向遍历-时间复杂度：O(n)，空间复杂度O(1)
    // （常数空间复杂度的括号匹配解法）
    // 利用两个计数器 left 和 right
    // 首先，从左到右遍历字符串，对于遇到的每个 (，left++，对于遇到的每个 ) ，right++
    // 1.当 right 比 left 大时，将 left 和 right 计数器同时变回 0
    // （右括号数大于左括号数时，一定不匹配）
    // 2.每当 left 与 right 相等时（此时一定括号匹配）
    // 计算当前有效字符串的长度，并且记录目前为止找到的最长子字符串
    // right 小于 left时，是一种待定的状态，可能之后会匹配到，也可能最后都没有匹配
    // 这种状态下可能会漏掉中间的有效括号，解决的方法是使用类似的方法，从右往左遍历
    public int longestValidParentheses2(String s) {
        int left = 0, right = 0, maxlength = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(')
                left++;
            else
                right++;
            if (left == right)
                maxlength = Math.max(maxlength, 2 * right);
            else if (right > left)
                left = right = 0;
        }

        left = right = 0;
        for (int i = s.length() - 1; i >= 0; i--) {
            if (s.charAt(i) == '(')
                left++;
            else
                right++;
            if (left == right)
                maxlength = Math.max(maxlength, 2 * left);
            else if (left > right)
                left = right = 0;
        }
        return maxlength;
    }

    // 方法三：动态规划
    // 看看就好，麻了
    public int longestValidParentheses3(String s) {
        int maxans = 0;
        int[] dp = new int[s.length()];
        for (int i = 1; i < s.length(); i++) {
            if (s.charAt(i) == ')') {
                if (s.charAt(i - 1) == '(')
                    dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
                else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(')
                    dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;

                maxans = Math.max(maxans, dp[i]);
            }
        }
        return maxans;
    }

    // 42. 接雨水
    // 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水
    // n == height.length
    // 0 <= n <= 3e4
    // 0 <= height[i] <= 1e5

    // 方法一：动态规划 保存左右最高位-时间复杂度：O(n)，空间复杂度：O(n)
    // 下标 i 处能接的雨水量 =
    // 下标 i 处的水能到达的最大高度（下标 i 两边的最大高度的最小值）- height[i]
    // 创建两个长度为 n 的数组 leftMax 和 rightMax
    // 对于 1 ≤ i < n-1 ，leftMax[i] 表示下标 i 及其左边的位置中，height 的最大高度
    // rightMax[i] 表示下标 i 及其右边的位置中，height 的最大高度
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0)
            return 0;

        int[] leftMax = new int[n];
        leftMax[0] = height[0];// 最左
        for (int i = 1; i < n; ++i)
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);

        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1];// 最右
        for (int i = n - 2; i >= 0; --i)
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);

        int ans = 0;
        for (int i = 0; i < n; ++i)
            ans += Math.min(leftMax[i], rightMax[i]) - height[i];

        return ans;
    }

    // 方法二：单调栈-时间复杂度：O(n)，空间复杂度：O(n)
    // 维护一个单调栈，单调栈存储的是下标
    // 满足从栈底到栈顶的下标对应的数组 height 中的元素递减（凹槽左边界）
    public int trap2(int[] height) {
        int ans = 0;
        Deque<Integer> stack = new LinkedList<Integer>();// 单调栈存储的是下标
        int n = height.length;
        for (int i = 0; i < n; ++i) {
            // 栈非空，或当前元素（凹槽右边界） > 栈顶元素
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                // 单调栈的特性保证栈顶2个元素分别是槽底和左边界
                int top = stack.pop();// 栈顶元素出栈（槽底）
                if (stack.isEmpty()) // 栈内唯一元素出栈
                    break;
                int left = stack.peek();// 凹槽左边界
                int currWidth = i - left - 1;// 凹槽宽度
                // 左右边界较小值-槽底
                int currHeight = Math.min(height[left], height[i]) - height[top];
                ans += currWidth * currHeight;
            }
            stack.push(i);
        }
        return ans;
    }

    // 方法三：双指针-时间复杂度：O(n)，空间复杂度：O(1)
    // 维护两个指针 left 和 right，以及两个变量 leftMax 和 rightMax，
    // 初始时 left = 0, right = n−1, leftMax = 0, rightMax = 0
    // 指针 left 只会向右移动，指针 right 只会向左移动
    // 移动指针的过程中维护两个变量 leftMax 和 rightMax 的值
    // 当两个指针没有相遇时，进行如下操作：
    // 使用 height[left] 和 height[right] 的值更新 leftMax 和 rightMax 的值
    // 1.如果 height[left] < height[right] ，则必有 leftMax < rightMax
    // 下标 left 处能接的雨水量等于 leftMax−height[left]
    // 将下标 left 处能接的雨水量加到能接的雨水总量，然后将 left 加 1（即向右移动一位）
    // 2.如果 height[left] ≥ height[right] ，则必有leftMax ≥ rightMax
    // 下标 right 处能接的雨水量等于 rightMax−height[right]
    // 将下标 right 处能接的雨水量加到能接的雨水总量，然后将 right 减 1（即向左移动一位）
    // 当两个指针相遇时，即可得到能接的雨水总量
    public int trap3(int[] height) {
        int ans = 0;
        int left = 0, right = height.length - 1;// 最左/最右下标
        int leftMax = 0, rightMax = 0;
        while (left < right) {// 左右指针相遇，跳出
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);// 每次循环只有一边最大边界得到更新
            // left right指针会停留在leftmax rightmax所在位置，直到另一个超过自己
            // 指针移动时，一定伴随着ans的更新，因此要确保可能存在的更高边界（max等待被超）
            if (height[left] < height[right]) {
                ans += leftMax - height[left];// （更小边界-底）
                ++left;// 更小一边移动
            } else {
                ans += rightMax - height[right];// （更小边界-底）
                --right;// 更小一边移动
            }
        }
        return ans;
    }

    // 84.柱状图中最大的矩形
    // 给定 n 个非负整数，表示柱状图中各个柱子的高度。每个柱子彼此相邻，且宽度为 1
    // 求在该柱状图中，能够勾勒出来的矩形的最大面积
    // 提示：
    // 1 <= heights.length <=105
    // 0 <= heights[i] <= 104

    // 方法一：单调栈 两次遍历-时空复杂度：O(n)
    // 从左至右（单调递增）、从右至左（单调递增）两次遍历
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int[] left = new int[n];// 包含当前柱子的最大矩形的左开区间
        int[] right = new int[n];// 包含当前柱子的最大矩形的右开区间

        Deque<Integer> mono_stack = new LinkedList<>();// 单调栈 单调递增，存放下标
        for (int i = 0; i < n; ++i) {
            // 栈非空，且当前元素小于栈顶元素（不满足单调递增），循环出栈直到满足单调递增

            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i])
                mono_stack.pop();

            if (mono_stack.isEmpty())
                left[i] = -1; // 1.第一个元素，2.当前为最小元素，左边全比他高
            else
                left[i] = mono_stack.peek(); // 左边比当前高，即为左开区间
            mono_stack.push(i);
        }

        mono_stack.clear();// 清空栈
        for (int i = n - 1; i >= 0; --i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i])
                mono_stack.pop();

            // 1.第一个被遍历的元素，2.当前为最小元素，右边全比他高
            right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
            mono_stack.push(i);
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) // 宽度 = 左右开区间差值 - 1
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);

        return ans;
    }

    // 方法二：单调栈 + 常数优化 只遍历一次-时空复杂度：O(n)
    // 位置 i 元素入栈时，确定了它的左边界。出栈时确定了右边界
    public int largestRectangleArea2(int[] heights) {
        int n = heights.length;
        int[] left = new int[n];
        int[] right = new int[n];
        Arrays.fill(right, n);// 初始为n 1.最后一个元素 2.右边全比他高（遍历完都没有出栈）

        Deque<Integer> mono_stack = new LinkedList<>();// 单调栈 单调递增，存放下标
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                right[mono_stack.peek()] = i;// 出栈时确定了右边界
                mono_stack.pop();
            }
            left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
            mono_stack.push(i);// 入栈时，确定了它的左边界
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) // 宽度 = 左右开区间差值 - 1
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);

        return ans;
    }

    // 方法二：（自己写的）单调栈 + 只遍历一次 + 引入边界方便计算 + 出栈入栈计算同步进行
    Deque<int[]> stkIndexHeight = new LinkedList<>();// 单调栈，单调递增
    int res = 0;
    int n;

    public int largestRectangleArea22(int[] heights) {
        n = heights.length;
        stkIndexHeight.push(new int[] { -1, -1 });// 左边界，一定不会出栈，则后续不用判断栈空
        for (int i = 0; i < n; i++) {
            int cur = heights[i];
            popAndCalculate(cur, i);
            stkIndexHeight.push(new int[] { i, cur });
        }
        popAndCalculate(0, n);// 右边界，使得栈中剩余元素出栈，出栈直至左边界为止，得到计算值
        return res;
    }

    private void popAndCalculate(int curHeight, int index) {
        while (curHeight < stkIndexHeight.peek()[1]) {
            int height = stkIndexHeight.pop()[1];
            int leftIndex = stkIndexHeight.peek()[0];
            int area = height * (index - leftIndex - 1);
            res = Math.max(res, area);
        }
    }

    // 85.最大矩形
    // 给定一个仅包含 0 和 1 、大小为 m x n 的二维二进制矩阵
    // 找出只包含 1 的最大矩形，并返回其面积
    // ||||j n →
    // i m ↓ □

    // 方法一：单调栈，84.找柱状图中最大的矩形的方法-时空复杂度：O(mn)
    public int maximalRectangle(char[][] matrix) {
        int m = matrix.length;
        if (m == 0)
            return 0;

        int n = matrix[0].length;
        int[][] height = new int[m][n];

        for (int j = 0; j < n; j++) // 统计第j行高度
            for (int i = 0; i < m; i++)
                if (matrix[i][j] == '1') { // 为0的不处理，高度一定是0
                    if (i == 0) // 防越界
                        height[i][j] = 1;
                    else
                        height[i][j] = height[i - 1][j] + 1;
                }

        // 84. 找柱状图中最大的矩形 方法一
        int ret = 0;
        for (int i = 0; i < m; i++) {// 对于每一行，使用基于 84.柱状图的方法

            int[] left = new int[n], right = new int[n]; // 左右开区间边界
            Deque<Integer> stk = new LinkedList<Integer>(); // 单调栈 单调递增，存放下标
            for (int j = 0; j < n; j++) {
                while (!stk.isEmpty() && height[i][stk.peek()] >= height[i][j])
                    stk.pop();
                left[j] = stk.isEmpty() ? -1 : stk.peek();
                stk.push(j);
            }
            stk.clear();
            for (int j = n - 1; j >= 0; j--) {
                while (!stk.isEmpty() && height[i][stk.peek()] >= height[i][j])
                    stk.pop();
                right[j] = stk.isEmpty() ? n : stk.peek();
                stk.push(j);
            }

            for (int j = 0; j < n; j++) {
                int width = right[j] - left[j] - 1;
                int area = width * height[i][j];
                ret = Math.max(ret, area);
            }
        }
        return ret;
    }

    // 方法一：（自己写的）单调栈 + 84. -时间复杂度：O(mn)，空间复杂度：O(n)
    // int res = 0;
    // Deque<int[]> stkIndexHeight = new LinkedList<>();
    public int maximalRectangle11(char[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        int[] heights = new int[n];
        for (int i = m - 1; i >= 0; i--) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '0')
                    heights[j] = 0;
                else
                    heights[j] = heights[j] + 1;
            }
            getMaxRectangle(heights);
        }
        return res;
    }

    private void getMaxRectangle(int[] heights) {
        int n = heights.length;
        stkIndexHeight.push(new int[] { -1, -1 });
        for (int i = 0; i < n; i++) {
            int curHeight = heights[i];
            while (curHeight < stkIndexHeight.peek()[1]) {
                int height = stkIndexHeight.pop()[1];
                int leftIndex = stkIndexHeight.peek()[0];
                int area = height * (i - leftIndex - 1);
                res = Math.max(res, area);
            }
            stkIndexHeight.push(new int[] { i, curHeight });
        }
        while (0 < stkIndexHeight.peek()[1]) {
            int height = stkIndexHeight.pop()[1];
            int leftIndex = stkIndexHeight.peek()[0];
            int area = height * (n - leftIndex - 1);
            res = Math.max(res, area);
        }
    }

    // 94.二叉树的中序遍历

    // 114.二叉树展开为链表
    // 给你二叉树的根结点 root ，请你将它展开为一个单链表：
    // 展开后的单链表应该同样使用 TreeNode ，其中 right 子指针指向链表中下一个结点，而左子指针始终为 null
    // 展开后的单链表应该与二叉树 先序遍历 顺序相同。
    // 进阶：你可以使用原地算法（O(1) 额外空间）展开这棵树吗？

    // 方法一：递归-前序遍历和展开为单链表分成了两步-空间复杂度O(n)
    public void flatten(TreeNode root) {
        List<TreeNode> list = new ArrayList<TreeNode>();// 结点为树结点的链表
        preorderTraversal(root, list);
        int size = list.size();

        // 展开为链表l，满足right 子指针指向链表中下一个结点，而左子指针始终为 null
        for (int i = 1; i < size; i++) {// 注意边界条件
            TreeNode prev = list.get(i - 1), curr = list.get(i);
            prev.left = null;
            prev.right = curr;
        }
    }

    public void preorderTraversal(TreeNode root, List<TreeNode> list) {
        if (root != null) {
            list.add(root);// 按前序遍历的顺序依次加入容器
            preorderTraversal(root.left, list);
            preorderTraversal(root.right, list);
        }
    }

    // 方法二：迭代-前序遍历和展开为单链表分成了两步-空间复杂度O(n)
    public void flatten2(TreeNode root) {
        List<TreeNode> list = new ArrayList<TreeNode>();
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        TreeNode node = root;
        while (node != null || !stack.isEmpty()) {
            while (node != null) {
                list.add(node);
                stack.push(node);
                node = node.left;
            }
            node = stack.pop();
            node = node.right;
        }

        // 展开为链表
        int size = list.size();
        for (int i = 1; i < size; i++) {
            TreeNode prev = list.get(i - 1), curr = list.get(i);
            prev.left = null;
            prev.right = curr;
        }
    }

    // 方法三：前序遍历和展开同步进行-空间复杂度O(n)
    public void flatten3(TreeNode root) {
        if (root == null)
            return;

        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        stack.push(root);
        TreeNode prev = null;
        while (!stack.isEmpty()) {
            TreeNode curr = stack.pop();
            if (prev != null) {
                prev.left = null;
                prev.right = curr;
            }
            TreeNode left = curr.left, right = curr.right;

            // 在遍历左子树之前就获得左右子结点的信息，并存入栈内
            // 子结点的信息就不会丢失，就可以将前序遍历和展开为单链表同时进行
            if (right != null)
                stack.push(right);
            if (left != null)
                stack.push(left);

            prev = curr;
        }
    }

    // 方法四：寻找前驱结点-空间复杂度O(1)、类比Morris遍历
    public void flatten4(TreeNode root) {
        TreeNode curr = root;
        while (curr != null) {
            if (curr.left != null) {
                TreeNode next = curr.left;
                TreeNode predecessor = next;
                while (predecessor.right != null) // root右孩子的前驱为左兄弟最右
                    predecessor = predecessor.right;
                // 展开单链表
                predecessor.right = curr.right;// 线索化
                curr.left = null;
                curr.right = next;// 先序遍历结点后继为左孩子
            }
            // 当前结点左结点为空，则访问右结点；不为空则访问的是后继结点
            curr = curr.right;
        }
    }

    // 155.最小栈
    // 设计一个支持 push ，pop ，top 操作，并能在常数时间内检索到最小元素的栈。
    // 实现 MinStack 类:
    // MinStack() 初始化堆栈对象。
    // void push(int val) 将元素val推入堆栈。
    // void pop() 删除堆栈顶部的元素。
    // int top() 获取堆栈顶部的元素。
    // int getMin() 获取堆栈中的最小元素。
    // 提示：
    // -231 <= val <= 231 - 1
    // pop、top 和 getMin 操作总是在 非空栈 上调用
    // push, pop, top, and getMin最多被调用 3 * 104 次

    class MinStack {
        Deque<Integer> stk = new LinkedList<>();
        Deque<Integer> minstk = new LinkedList<>();

        public MinStack() {
            stk.push(Integer.MAX_VALUE);
            minstk.push(Integer.MAX_VALUE);
        }

        public void push(int val) {
            stk.push(val);
            minstk.push(Math.min(minstk.peek(), val));
        }

        public void pop() {
            stk.pop();
            minstk.pop();
        }

        public int top() {
            return stk.peek();
        }

        public int getMin() {
            return minstk.peek();
        }
    }

    // 234.回文链表

    // 394.字符串解码
    // 给定一个经过编码的字符串，返回它解码后的字符串。
    // 编码规则为: k[encoded_string]，表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
    // 你可以认为输入字符串总是有效的；输入字符串中没有额外的空格，且输入的方括号总是符合格式要求的。
    // 此外，你可以认为原始数据不包含数字，所有的数字只表示重复的次数 k ，例如不会出现像 3a 或 2[4] 的输入。
    // 提示：
    // 1 <= s.length <= 30
    // s 由小写英文字母、数字和方括号 '[]' 组成
    // s 保证是一个 有效 的输入。
    // s 中所有整数的取值范围为 [1, 300] 

    // 方法一：栈-时间复杂度：O(n)，空间复杂度：O(n)
    int ptr;

    public String decodeString(String s) {
        Deque<String> stk = new LinkedList<>();// 字符串的栈，用双端队列来模拟栈操作，方便从栈底向栈顶遍历
        ptr = 0;// 指向s的下标位置

        while (ptr < s.length()) {
            char cur = s.charAt(ptr);
            if (Character.isDigit(cur)) {// 读到数字
                String digits = getDigits(s);// 获取一个数字并进栈
                stk.offerLast(digits);
            } else if (Character.isLetter(cur) || cur == '[') // 读到字母或左括号[
                stk.offerLast(String.valueOf(s.charAt(ptr++))); // 长度为1的字符串进栈
            else {// 读到右括号]
                ++ptr;
                // 为了和Deque共用一个getString函数，声明的好怪
                LinkedList<String> sub = new LinkedList<String>();// 存储一连串单字母串
                while (!"[".equals(stk.peekLast())) // []内一定全是字母
                    sub.offerLast(stk.pollLast());
                Collections.reverse(sub);// 容器反转

                stk.pollLast();// 左括号出栈
                // 此时栈顶为当前 sub 对应的字符串应该出现的次数
                int repTime = Integer.parseInt(stk.pollLast());// string to int
                StringBuilder t = new StringBuilder();
                String repString = getString(sub);
                // 构造字符串
                while (repTime-- > 0) // repTime>0
                    t.append(repString);

                stk.offerLast(t.toString());// 将构造好的字符串入栈
            }
        }

        return getString(stk);
    }

    public String getDigits(String s) {
        StringBuilder ret = new StringBuilder();// 一位或多位数字组合成字符串
        while (Character.isDigit(s.charAt(ptr)))
            ret.append(s.charAt(ptr++));
        return ret.toString();
    }

    public String getString(Deque<String> v) {
        StringBuilder ret = new StringBuilder();
        for (String s : v)
            ret.append(s);
        return ret.toString();
    }

    // 方法一：（自己写的）栈-时间复杂度：O(n)，空间复杂度：O(n)
    // 更简单易理解
    public String decodeString11(String s) {
        Deque<String> stk = new LinkedList<>();
        int index = 0;
        int repeatTime = 0;

        while (index != s.length()) {
            char curr = s.charAt(index);
            index++;

            if (Character.isDigit(curr)) // 计算需重复次数
                repeatTime = repeatTime * 10 + (curr - '0');
            else if (curr == '[') {// 重复次数与 [ 入栈，重复次数置0
                stk.offerLast(String.valueOf(repeatTime));
                repeatTime = 0;
                stk.offerLast("[");
            } else if (Character.isLetter(curr))// 遍历到字母，直接入栈
                stk.offerLast(String.valueOf(curr));
            else {// 遍历到]，字符串出栈，翻转，得到重复内容，[出栈，重复次数出栈，处理后的字符串入栈
                List<String> list = new ArrayList<>();
                StringBuilder repeatContent = new StringBuilder();
                while (stk.peekLast() != "[")
                    list.add(stk.pollLast());
                Collections.reverse(list);
                for (String str : list)
                    repeatContent.append(str);

                stk.pollLast();
                repeatTime = Integer.valueOf(stk.pollLast());

                StringBuilder ans = new StringBuilder();
                while (repeatTime != 0) {
                    ans.append(repeatContent);
                    repeatTime--;
                }
                stk.offerLast(ans.toString());
            }
        }

        StringBuilder res = new StringBuilder();
        while (!stk.isEmpty())
            res.append(stk.pollFirst());
        return res.toString();
    }

    // 方法二：递归-时间复杂度：O(n)，空间复杂度：O(n)
    String src;
    // int ptr;

    public String decodeString2(String s) {
        src = s;
        ptr = 0;// 指向字符串下标
        return getString();
    }

    public String getString() {
        if (ptr == src.length() || src.charAt(ptr) == ']')
            // String -> EPS
            return "";// 递归到头

        char cur = src.charAt(ptr);
        int repTime = 1;
        String ret = "";

        if (Character.isDigit(cur)) {// 数字[字母] // String -> Digits [ String ] String
            repTime = getDigits(); // 解析 Digits
            ++ptr;// 过滤左括号
            String str = getString(); // 解析 String
            ++ptr; // 过滤右括号
            while (repTime-- > 0) // 构造字符串
                ret += str;

        } else if (Character.isLetter(cur)) // 字母
            // String -> Char String
            // 解析 Char
            ret = String.valueOf(src.charAt(ptr++));// 单字母字符串

        return ret + getString();// 精髓之一
    }

    public int getDigits() {
        int ret = 0;
        while (ptr < src.length() && Character.isDigit(src.charAt(ptr)))
            ret = ret * 10 + src.charAt(ptr++) - '0';// 去掉字符串末尾终止符

        return ret;
    }

    // 方法二：（自己写的）递归-时间复杂度：O(n)，空间复杂度：O(n)
    // 更简单易理解
    int index = 0;
    String str;

    public String decodeString22(String s) {
        this.str = s;
        StringBuilder res = dfs();
        return res.toString();
    }

    private StringBuilder dfs() {
        StringBuilder ans = new StringBuilder();
        int repeatTime = 0;
        while (index != str.length()) {// index至末尾，最底层dfs循环结束，返回结果
            char curr = str.charAt(index);
            index++;
            if (Character.isDigit(curr))// 计算需重复次数
                repeatTime = repeatTime * 10 + (curr - '0');
            else if (curr == '[') {// 进入下一层递归，得到[]中的内容，并重复放入ans
                StringBuilder repeatContent = dfs();
                while (repeatTime != 0) {
                    ans.append(repeatContent);
                    repeatTime--;
                }
            } else if (Character.isLetter(curr))// 遍历到字母，直接放入ans
                ans.append(curr);
            else // 遍历到]，退出循环，返回[]中的内容
                break;
        }
        return ans;
    }

    // 581.最短无序连续子数组

    // 739.每日温度
    // 给定一个整数数组 temperatures ，表示每天的温度，
    // 返回一个数组 answer ，其中 answer[i] 是指在第 i 天之后，才会有更高的温度。
    // 如果气温在这之后都不会升高，请在该位置用 0 来代替。
    // 提示：
    // 1 <= temperatures.length <= 105
    // 30 <= temperatures[i] <= 100

    // 每日温度-1.单调栈-时间复杂度O(n)
    // 维护一个存储下标的单调栈，从栈底到栈顶的下标对应的温度列表中的温度依次递减
    // 如果一个下标在单调栈里，则表示尚未找到下一次温度更高的下标
    public int[] dailyTemperatures(int[] temperatures) {
        int length = temperatures.length;
        int[] ans = new int[length];
        Deque<Integer> stack = new LinkedList<Integer>();// 存储下标的单调栈（单调递减）
        for (int i = 0; i < length; i++) {// 从头开始遍历数组
            int temperature = temperatures[i];
            while (!stack.isEmpty() && temperature > temperatures[stack.peek()]) {
                int prevIndex = stack.pop();
                ans[prevIndex] = i - prevIndex;
            }
            stack.push(i);// 栈为空，或入栈温度低于栈顶温度
        }
        return ans;
    }

    // 每日温度-2.暴力法-时间复杂度O(mn)
    // 对于温度列表中的每个元素 temperatures[i]，需要找到最小的下标 j
    // 使得 i < j 且 temperatures[i] < temperatures[j]

}

// 贪心
class GreedyAlgorithmSol {
    // 11. 盛最多水的容器
    // 给定 n 个非负整数 a1，a2，...，an，每个数代表坐标中的一个点 (i, ai)
    // 在坐标内画 n 条垂直线，垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)

    // 找出其中的两条线，使得它们与 x 轴共同构成的容器可以容纳最多的水

    // 方法一：双指针-时间复杂度O(n)，空间复杂度：O(1)
    // 容纳的水量是由两个指针指向的数字中较小值 * 指针之间的距离决定的
    // 如果移动数字较大的那个指针，那么前者「两个指针指向的数字中较小值」不会增加
    // 后者「指针之间的距离」会减小，那么这个乘积会减小
    // 因此，移动数字较大的那个指针是不合理的，应当移动数字较小的那个指针
    public int maxArea(int[] height) {
        int l = 0, r = height.length - 1;// 最末元素索引
        int ans = 0;

        while (l < r) {
            int area = Math.min(height[l], height[r]) * (r - l);
            ans = Math.max(ans, area);
            if (height[l] <= height[r])// 移动更小的一边
                ++l;
            else
                --r;
        }
        return ans;
    }

    // 55. 跳跃游戏
    // 给定一个非负整数数组 nums ，你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度（可跳范围是一个区间）
    // 判断你是否能够到达最后一个下标（审题！并没有让你求到达的路径）

    // 方法一：贪心-时间复杂度O(n)
    public boolean canJump(int[] nums) {
        int n = nums.length;
        int rightMost = 0;// 最右可达
        for (int i = 0; i < n; ++i) {
            if (i <= rightMost) {// 下标i可达，才可计算下一跳的最右可达
                rightMost = Math.max(rightMost, i + nums[i]);
                if (rightMost >= n - 1)// 最右可达大于最后一个下标，则能够到达
                    return true;
            } // 提前跳出省略的时间也微乎其微
        }
        return false;
    }

    // 253. 会议室 II
    // 给定一个会议时间安排的数组，每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],…] (si < ei)
    // 为避免会议冲突，同时要考虑充分利用会议室资源，请计算至少需要多少间会议室，才能满足这些会议安排

    // 方法一：排序 + 优先队列、最小堆-时间复杂度：O(nlogn)
    // 各会议按开始时间排序，每当有新会议时，就遍历所有房间，如果有空房间
    // （新会议的开始时间 >= 堆顶会议的结束时间：因为堆顶是最早结束的，如果它还不结束，则其他的都不结束），不用开新房间了
    // 更新旧房间的结束时间，否则开新房间。使用优先队列，最小堆，堆顶为结束时间最早的会议
    // arr[第几个会议][0开始时间 1结束时间]
    public int minMeetingRooms(int[][] arr) {
        if (arr.length == 0)
            return 0;

        // 优先队列，最小堆，堆顶为结束时间最早的会议，存放的是各房间的结束时间
        PriorityQueue<Integer> q = new PriorityQueue<>();
        Arrays.sort(arr, (meeting1, meeting2) -> meeting1[0] - meeting2[0]);// 按照开始时间排序，增添新房间时，一定是找不到已有房间安排当前会议
        q.offer(arr[0][1]);// 第一个会议的结束时间入队

        for (int i = 1; i < arr.length; i++) {
            System.out.println(q.peek());
            if (arr[i][0] >= q.peek())// 新会议的开始时间 >= 堆顶会议的结束时间（堆顶是最早结束的），更新旧房间的结束时间
                q.poll();// 堆顶房间出队
            q.offer(arr[i][1]);// 更新出队房间的结束时间，或新房间入队
        }
        return q.size();// 队列中的元素数即房间数
    }

    // 方法二：排序 + 双指针法-时间复杂度：O(nlogn)
    // 统计同一时刻进行的最大会议的数量（也即需要的最多房间），分别按照开始时间和结束时间排序，遍历两个数组
    public int minMeetingRooms2(int[][] arr) {
        int[] begin = new int[arr.length];
        int[] end = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            begin[i] = arr[i][0];
            end[i] = arr[i][1];
        }
        Arrays.sort(begin);
        Arrays.sort(end);
        int i = 0, j = 0;// 分别指向开始时间和结束时间的下标
        int max = 0;// 同一时刻进行的最大会议的数量
        int cur = 0;// 当前时刻进行的最大会议的数量，第j场会议结束前有多少场会议开始了（肯定包含第j场会议本身）
        while (i < arr.length && j < arr.length) {
            if (begin[i] < end[j]) {
                cur++;
                i++;
            } else if (begin[i] == end[j]) {
                i++;
                j++;
            } else {// begin[i] > end[j]
                cur--;
                j++;
            }
            max = Math.max(max, cur);
        }
        return max;

    }

    // 406. 根据身高重建队列
    // 假设有打乱顺序的一群人站成一个队列，数组 people 表示队列中一些人的属性（不一定按顺序）
    // 每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ，前面 正好 有 ki 个身高大于或等于 hi 的人
    // 请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue
    // 其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性（queue[0] 是排在队列前面的人）
    // 题目数据确保队列可以被重建

    // 根据身高重建队列-1.从低到高考虑-时间复杂度O(n2)
    // 矮个子先放
    // 出发点：把高个子拿掉，影响矮个子的k值，但矮个子可根据k值（留空给高个子），提前决定的下标位置不会变（数组）
    // 高个子的放入会影响矮个子的k值，只要矮个子优先放入，根据k值，提前留空，矮个子的下标就不会变化
    // 相同身高的情况下：k值大的优先（根据k值留空给k值更小的）
    public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people, new Comparator<int[]>() {
            public int compare(int[] person1, int[] person2) {
                if (person1[0] != person2[0])
                    return person1[0] - person2[0];// 身高h升序排列
                else
                    return person2[1] - person1[1];// 身高h相同的情况下k降序
            }
        });

        int n = people.length;
        int[][] ans = new int[n][];
        for (int[] person : people) {
            int spaces = person[1] + 1;
            for (int i = 0; i < n; ++i)
                if (ans[i] == null) {// 遇空则跳（空位子留给后面放入的高个子）
                    --spaces;
                    if (spaces == 0) {// 直到space为0则放入
                        ans[i] = person;
                        break;
                    }
                }
        }
        return ans;
    }

    // 根据身高重建队列-2.从高到低考虑-时间复杂度O(nlogn)
    // 高个子先插入
    // 出发点：把矮个子拿掉不影响高个子的k值，但高个子无法根据k值，提前决定下标位置，只能根据k值决定相对位置（链表）
    // 矮个子插入不影响高个子的k值，高个子优先插入，根据k值确定好相对位置（链表的插入）
    public int[][] reconstructQueue2(int[][] people) {
        Arrays.sort(people, new Comparator<int[]>() {
            public int compare(int[] person1, int[] person2) {
                if (person1[0] != person2[0])
                    return person2[0] - person1[0];// 身高h降序排列
                else
                    return person1[1] - person2[1];// 身高h相同的情况下k升序
            }
        });

        List<int[]> ans = new ArrayList<int[]>();// 空表
        for (int[] person : people)
            ans.add(person[1], person);// 从高个子开始，从k位置插入
        return ans.toArray(new int[ans.size()][]);
    }

    // 581. 最短无序连续子数组
    // 给你一个整数数组 nums ，你需要找出一个 连续子数组 ，如果对这个子数组进行升序排序，那么整个数组都会变为升序排序。
    // 请你找出符合题意的 最短 子数组，并输出它的长度。
    // 提示：
    // 1 <= nums.length <= 104
    // -105 <= nums[i] <= 105
    // 进阶：你可以设计一个时间复杂度为 O(n) 的解决方案吗？

    // 方法一：不使用额外空间（贪心）-时间复杂度O(n)，空间复杂度：O(1)
    // 思想：无序子数组中最小元素的正确位置可以决定左边界，最大元素的正确位置可以决定右边界
    // 实现：两趟遍历：
    // 从左至右遍历，记录最大值，当前值小于最大值，则更新right
    // 从右至左遍历，记录最小值，当前值大于最小值，则更新left
    public int findUnsortedSubarray(int[] nums) {
        int n = nums.length;
        int maxn = Integer.MIN_VALUE;
        int right = -1;
        int minn = Integer.MAX_VALUE;
        int left = -1;
        for (int i = 0; i < n; i++) {
            // 更新从左到右出现的最大值，理想情况下，数字序列是递增的，则每一个num 都应该大于之前出现的最大值 maxn
            if (maxn > nums[i])
                // 这种遍历方式可以一直找到最后一个nums[i],其值小于 max(nums[0]~nums[i])
                // 从这个值往后的所有值 nums[i+1] ~ nums[n-1] ，都大于 max(nums[0]~nums[i])
                right = i;
            else
                maxn = nums[i];// 理想情况：更新[0~i]中的最大值

            // 更新从右到左出现的最小值，理想情况下，数字序列是递减的，则每一个num 都应该小于之前出现的最小值< minn
            if (minn < nums[n - i - 1])
                // 这种遍历方式可以一直找到最后一个nums[i],其值小于 min(nums[i]~nums[n-1])
                // 从这个值往前的所有值 nums[0] ~ nums[i-1] ，都小于 min(nums[i]~nums[n-1])
                left = n - i - 1;
            else
                minn = nums[n - i - 1];// 理想情况：更新[n-i-1~n-1]中的最小值

        }
        return right == -1 ? 0 : right - left + 1;// right==-1则整个数组有序，无最短无序子数组
    }

    // 方法一：（自己想的，稍复杂，但也是O(n)）贪心-时间复杂度O(n)，空间复杂度：O(1)
    // 实现：两趟遍历：
    // 从左至右遍历，记录最大值，当前值小于最大值，则记录当前值，取得最小的不满足升序的值，找到其合适位置
    // 从右至左遍历，记录最小值，当前值大于最小值，则记录当前值，取得最大的不满足降序的值，找到其合适位置

    // 方法二：单调栈（两趟遍历）-时间复杂度O(n)，空间复杂度：O(n)
    public int findUnsortedSubarray2(int[] nums) {
        Deque<Integer> stk = new LinkedList<>();// 实际存放的是数组的下标
        int l = nums.length; // 左边界
        int r = 0; // 右边界

        // 找最小左边界
        for (int i = 0; i < nums.length; i++) {
            while (!stk.isEmpty() && nums[i] < nums[stk.peek()])
                // 直到栈内元素<=当前元素，此时l=当前元素应该插入的位置
                l = Math.min(l, stk.pop()); // 记录最左的左边界

            stk.push(i);
        }
        // 找最大右边界
        for (int i = nums.length - 1; i >= 0; i--) {
            while (!stk.isEmpty() && nums[i] > nums[stk.peek()])
                // 直到栈内元素>=当前元素，此时l=当前元素应该插入的位置
                r = Math.max(r, stk.pop()); // 记录最右的右边界

            stk.push(i);
        }

        if (l < r)
            return r - l + 1;
        else
            return 0;
    }

    // 方法二：单调栈（自己写的，一趟遍历）-时间复杂度O(n)，空间复杂度：O(n)
    public int findUnsortedSubarray22(int[] nums) {
        Deque<Integer> stkIndex = new LinkedList<>();// 单调栈 存放索引
        int left = Integer.MAX_VALUE;
        int right = 0;
        int outMax = Integer.MIN_VALUE;// 出栈过的最大元素，方便找到其正确位置
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            int outIndex = -1;// 出栈元素索引
            while (!stkIndex.isEmpty() && nums[stkIndex.peek()] > num) {
                outIndex = stkIndex.pop();
                outMax = Math.max(outMax, nums[outIndex]);
            }
            if (outIndex != -1)// left记录第一个出栈的元素索引
                left = Math.min(left, outIndex);
            if (num < outMax)// 找到 出栈过的最大元素的正确位置
                right = i;
            stkIndex.push(i);
        }

        if (left - right == Integer.MAX_VALUE)
            return 0;
        else
            return right - left + 1;
    }

    // 方法三：排序-时间复杂度O(nlog n)，空间复杂度：O(n)
    // 找到最小/最大元素正确的位置，即最短无序连续子数组的边界
    public int findUnsortedSubarray3(int[] nums) {
        int n = nums.length;
        int[] numsSorted = Arrays.copyOf(nums, n); // 复制原数组
        Arrays.sort(numsSorted);

        int left = 0; // 左边界
        int right = n - 1; // 右边界
        while (left < n && numsSorted[left] == nums[left])
            left++;

        while (right >= 0 && numsSorted[right] == nums[right])
            right--;

        if (left < right)
            return right - left + 1;
        else
            return 0;
    }

    // 621. 任务调度器
    // 给定一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务
    // 任务可以以任意顺序执行，并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间，CPU 可以完成一个任务，或者处于待命状态
    // 然而，两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间，因此至少有连续 n 个单位时间内 CPU 在执行不同的任务，或者在待命状态
    // 你需要计算完成所有任务所需要的 最短时间
    // 提示：
    // 1 <= task.length <= 104
    // tasks[i] 是大写英文字母
    // n 的取值范围为 [0, 100]

    // 方法一：模拟-时间复杂度O(nm) n = 任务数，m = 任务种类数
    // 不断地选择不在冷却中并且剩余执行次数最多的那个任务
    // 用二元组（顺序表ArrayList 类似c++的vector容器） (nextValidi, resti) 表示第 i 个任务
    // 其中 nextValidi 表示其因冷却限制最早可以执行的时间
    // resti 表示其剩余执行次数。初始时，所有的 nextValidi 均为 1，而 resti即为任务 i 在数组 tasks 中出现的次数

    public int leastInterval(char[] tasks, int n) {
        Map<Character, Integer> freq = new HashMap<Character, Integer>();// 统计各任务需执行次数

        for (char ch : tasks)
            freq.put(ch, freq.getOrDefault(ch, 0) + 1);// 更新任务ch当前的执行次数，默认值为0

        int m = freq.size();// 任务种类数
        List<Integer> nextValid = new ArrayList<Integer>();// 各任务因冷却限制最早可以执行的时间
        List<Integer> rest = new ArrayList<Integer>();// 各任务剩余执行次数
        Set<Map.Entry<Character, Integer>> entrySet = freq.entrySet();// HashMap转集合
        for (Map.Entry<Character, Integer> entry : entrySet) {// entry：键值对
            int value = entry.getValue();
            nextValid.add(1);// 初始值为1，之后为绝对时间
            rest.add(value);
        }

        int time = 0;// 最短所需时间
        for (int i = 0; i < tasks.length; ++i) {
            ++time;
            int minNextValid = Integer.MAX_VALUE;// 最早可以执行的绝对时间的最小值
            for (int j = 0; j < m; ++j) // 对于各任务
                if (rest.get(j) != 0)
                    minNextValid = Math.min(minNextValid, nextValid.get(j));

            time = Math.max(time, minNextValid);

            int best = -1;// 不在冷却中并且剩余执行次数最多的那个任务
            for (int j = 0; j < m; ++j)// 对于各任务
                if (rest.get(j) != 0 && nextValid.get(j) <= time)// 跳过剩余执行次数为0的任务和已被执行过的任务
                    if (best == -1 || rest.get(j) > rest.get(best))// 剩余执行次数最多的那个任务
                        best = j;
            System.out.println(best);
            nextValid.set(best, time + n + 1);// 更新（只用更新best任务的）最早可以执行的绝对时间
            rest.set(best, rest.get(best) - 1);// best任务剩余执行次数-1
        }

        return time;
    }

    // 方法二：构造-时间复杂度O(n+m) n = 任务数，m = 任务种类数
    // 依次考虑：具有最多执行次数maxExec的任务，具有最多执行次数maxExec的多个任务
    // 得出表达式 (maxExec − 1) (n + 1) + maxCount 矩形（行数 × 列数）+ (maxExec − 1)余下的maxCount个
    // n+1：n为冷却时间，则窗口大小为n+1（窗口内不能有相同的任务）
    // 在任意的情况下，需要的最少时间就是 (maxExec−1)(n+1)+maxCount （如果该值为较大值，则存在待命）
    // 和 ∣task∣（如果该值为较大值，不存在待命）中的较大值
    public int leastInterval2(char[] tasks, int n) {
        Map<Character, Integer> freq = new HashMap<Character, Integer>();// 统计各任务需执行次数
        int maxExec = 0;// 最多的执行次数
        for (char ch : tasks) {
            int exec = freq.getOrDefault(ch, 0) + 1;// 任务ch当前的执行次数，默认值为0
            freq.put(ch, exec);// 更新key的value
            maxExec = Math.max(maxExec, exec);
        }

        int maxCount = 0;// 具有最多执行次数maxExec的任务数量
        Set<Map.Entry<Character, Integer>> entrySet = freq.entrySet();// HashMap转集合Set
        for (Map.Entry<Character, Integer> entry : entrySet) // entry：key-value键值对
            if (entry.getValue() == maxExec)
                ++maxCount;

        return Math.max((maxExec - 1) * (n + 1) + maxCount, tasks.length);
    }

    // 方法二：构造（自己写的，根据题意，可以不使用map）-时间复杂度O(n+m) n = 任务数，m = 任务种类数
    public int leastInterval22(char[] tasks, int n) {
        int[] counts = new int[26];
        int maxCount = 0;
        int maxCountTaskNum = 0;
        for (char task : tasks) {
            int index = task - 'A';
            counts[index] += 1;
        }
        maxCount = Arrays.stream(counts).max().getAsInt();
        for (int count : counts)
            if (count == maxCount)
                maxCountTaskNum += 1;

        return Math.max(tasks.length, (maxCount - 1) * (n + 1) + maxCountTaskNum);
    }
}

// 位运算
class BitOperationSol {
    // 78. 子集
    // 给你一个整数数组 nums ，数组中的元素 互不相同 。返回该数组所有可能的子集（幂集）
    // 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集
    // 1 <= nums.length <= 10
    // -10 <= nums[i] <= 10
    // nums 中的所有元素 互不相同

    // 方法一：迭代法实现子集枚举（位运算）-时间复杂度O(n×2^n)
    // 用 1 表示「在子集中」，0 表示「不在子集中」
    // 可以发现 0/10/1 序列对应的二进制数正好从 0 到 2^n - 1
    // 可以枚举mask∈[0,2^n−1]，mask 的二进制表示是一个 0/1 序列，可以按照这个 0/1 序列在原集合当中取数。
    // 当枚举完所有 2^n-1 个mask，也就能构造出所有的子集
    List<List<Integer>> subsets(int[] nums) {
        List<Integer> t = new ArrayList<Integer>();
        List<List<Integer>> ans = new ArrayList<List<Integer>>();

        int n = nums.length;
        for (int mask = 0; mask < (1 << n); ++mask) {
            t.clear();// 表t清空
            for (int i = 0; i < n; ++i)
                if ((mask & (1 << i)) != 0)
                    t.add(nums[i]);// 从末尾位开始，若为第i位为1则在t中加入对应元素（共n位）
            ans.add(new ArrayList<Integer>(t));
        }
        return ans;
    }

    // 方法二：递归法实现子集枚举（回溯、剪枝）-时间复杂度O(n×2^n)
    // 类似于树的深度优先遍历
    // 0左子树 1右子树
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    List<List<Integer>> subsets2(int[] nums) {
        dfs(0, nums);
        return ans;
    }

    void dfs(int cur, int[] nums) {
        if (cur == nums.length) {// 此时所有位已枚举完成，（回溯、剪枝）
            ans.add(new ArrayList<Integer>(t));
            return;
        }
        // 选择加入当前位置cur的元素
        t.add(nums[cur]);// 置于末尾
        dfs(cur + 1, nums);

        // 选择不加入当前位置cur的元素
        t.remove(t.size() - 1);// 移除末尾元素
        dfs(cur + 1, nums);
    }

    // 136. 只出现一次的数字
    // 给定一个非空整数数组，除了某个元素只出现 一次 以外，其余每个元素均出现 两次 。找出那个只出现了一次的元素

    // 1.位运算-空间复杂度O(1)
    // 异或运算的性质：a^a=0 a^0=a
    int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) // 对数组内元素全做异或，则最后得到的是只出现一次的的元素
            single ^= num;
        return single;
    }

    // 2.集合-空间复杂度O(n)
    // 遍历数组，Set中没有该数组则加入Set，有则删除（第二次出现）
    int singleNumber2(int[] nums) {
        Set<Integer> numsSet = new HashSet<Integer>();
        for (int num : nums) {
            if (!numsSet.contains(num))
                numsSet.add(num);
            else
                numsSet.remove(num);
        }
        return numsSet.hashCode();// 返回此集合的哈希代码值，集合的哈希代码定义为集合中元素的哈希代码之和
    }

    // 3.哈希表-空间复杂度O(n)
    // HashMap存储每个数字及该数字出现的次数
    int singleNumber3(int[] nums) {
        Map<Integer, Integer> numsMap = new HashMap<Integer, Integer>();
        for (int num : nums)
            numsMap.put(num, numsMap.getOrDefault(num, 0) + 1);// 统计各数字出现次数，默认值为0

        Set<Map.Entry<Integer, Integer>> entrySet = numsMap.entrySet();// HashMap转集合
        for (Map.Entry<Integer, Integer> entry : entrySet) // entry：键值对
            if (entry.getValue() == 1)
                return entry.getKey();
        return -1;
    }

    // 4.数组转集合-空间复杂度O(n)
    // 集合*2-数组=只出现一次的数字
    int singleNumber4(int[] nums) {
        // 数组转Set
        int numsSum = 0;
        Set<Integer> numsSet = new HashSet<Integer>();
        for (int num : nums) {
            numsSum += num;
            numsSet.add(num);
        }
        return numsSet.hashCode() * 2 - numsSum;
    }

    // 287. 寻找重复数（数据范围有一定特殊性，才可以实现空间复杂度O(1)）
    // 给定一个包含 n + 1 个整数的数组 nums ，其数字都在 1 到 n 之间（包括 1 和 n），可知至少存在一个重复的整数
    // 假设 nums 只有 一个重复的整数 ，找出 这个重复的数
    // nums 中 只有一个整数 出现 两次或多次 ，其余整数均只出现 一次
    // nums.length == n + 1
    // 1 <= nums[i] <= n
    // 你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间，即空间复杂度O(1)

    // 方法一：二进制-时间复杂度O(nlogn)
    // 正确性证明：
    // 数组中 target 出现了两次，则每个整数都有且仅有出现一次
    // 数组中 target 出现了三次及以上，那么必然有一些数不在 nums 数组中了，相当于用 target 去替换了这些数
    // 考虑替换的时候对 x 的影响，满足x > y 置为1（精髓所在）
    int findDuplicate(int[] nums) {
        int len = nums.length;// 对应n+1个整数
        int ans = 0;
        int bit_max = 31;// 对应数组长度所需的最大幂-1，整数的最大幂-1=31
        // 确定最高位，减少计算量（数组元素上界 = n）
        while (((len - 1) >> bit_max) == 0)
            bit_max -= 1;

        // 依次统计二进制数（从右往左数）的第bit位
        for (int bit = 0; bit <= bit_max; ++bit) {
            int x = 0;// 数组中所有数的第bit位之和
            int y = 0;// 0-n的第bit位之和
            for (int i = 0; i < len; ++i) {
                if ((nums[i] & (1 << bit)) != 0)// 第bit位为1
                    x += 1;
                if (i >= 1 && ((i & (1 << bit)) != 0))// 0到n的第bit位
                    y += 1;
            }

            if (x > y)// x > y时，第bit位置为1
                ans |= 1 << bit;// 或运算
        }
        return ans;
    }

    // 方法二：二分查找（左右边界不是数组的首尾元素，而是为了确定target的左右边界）-时间复杂度O(nlogn)
    // 定义 cnt[i] 表示 nums 数组中小于等于 i 的数有多少个
    // 假设重复的数是 target，那么 [1,target−1] 里的所有数满足cnt[i] ≤ i
    // [target,n] 里的所有数满足cnt[i] > i，具有单调性
    int findDuplicate2(int[] nums) {
        int len = nums.length;// 对应n+1个整数
        int l = 1, r = len - 1;// 左右指针
        int ans = -1;
        while (l <= r) {// 二分查找 logn趟
            int mid = (l + r) / 2;
            int cnt = 0;
            for (int i = 0; i < len; ++i)
                if (nums[i] <= mid)
                    cnt++;
            // [1,target−1] [target,n]
            if (cnt <= mid)// 则位于左半区间
                l = mid + 1;
            else {// 位于右半区间
                r = mid - 1;
                ans = mid;// 只有右半区间才包含正确的ans值，不管最后是从左半区间还是右半区间退出循环，ans值最后一次更新都必须来自右半区间
            }
        }
        return ans;
    }

    // 方法三：快慢指针-时间复杂度O(n)
    // 「Floyd 判圈算法」（又称龟兔赛跑算法），它是一个检测链表是否有环的算法
    // 对 nums 数组建图，每个位置 i 连一条 i→nums[i] 的边
    // 由于存在的重复的数字 target，因此 target 这个位置一定有起码两条指向它的边
    // 因此整张图一定存在环，且我们要找到的 target 就是这个环的入口
    // 先设置慢指针 slow 和快指针 fast ，慢指针每次走一步，快指针每次走两步
    // 根据「Floyd判圈算法」，两个指针在有环的情况下一定会相遇，
    // 此时再将 slow 放置起点 0，两个指针每次同时移动一步，相遇的点就是答案
    int findDuplicate3(int[] nums) {
        int slow = 0, fast = 0;// 快慢指针，指向nums对应下标的值
        do {// 1 ≤ nums[i] ≤ n，而nums长度为n+1，索引值为[0,n]，指针值为下一步的索引值
            slow = nums[slow];// 每次走一步
            fast = nums[nums[fast]];// 每次走两步
        } while (slow != fast);// 值相等跳出，此时有可能停在同一个值上，也可能停在ans值对应的两个位置上
        slow = 0;
        while (slow != fast) {
            slow = nums[slow];
            fast = nums[fast];
        }
        return slow;
    }

    // 338. 比特位计数
    // 给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ，计算其二进制数中的 1 的数目并将它们作为数组返回
    // 进阶:
    // 时间复杂度O(n)
    // 空间复杂度O(n)
    // 在C++或任何其他语言中不使用任何内置函数（如 C++ 中的 __builtin_popcount）来执行此操作

    // 1.java内置函数Integer.bitCount-时间复杂度O(nlogn)-2ms
    int[] countBits(int n) {
        int[] ans = new int[n + 1];
        for (int i = 0; i <= n; i++)
            ans[i] = Integer.bitCount(i);// 原理：分治，5次2^5=32
        return ans;
    }

    // 2.Brian Kernighan 算法-对 0 到 n 的每个整数直接计算「一比特数」-时间复杂度O(nlogn)-2ms
    // 利用 Brian Kernighan 算法，可以在一定程度上进一步提升计算速度
    // Brian Kernighan 算法的原理是：对于任意整数 x，令 x = x & (x − 1)，该运算将 x 的二进制表示的最后一个 1 变成 0
    // 因此，对 x 重复该操作，直到 x 变成 0，则操作次数即为 x 的「一比特数」
    int[] countBits2(int n) {
        int[] bits = new int[n + 1];
        for (int i = 0; i <= n; i++)
            bits[i] = countOnes(i);
        return bits;
    }

    int countOnes(int x) {
        int ones = 0;
        while (x > 0) {// 直到没有1，全为0
            x &= (x - 1);// 将 x 的二进制表示的最后一个 1 变成 0
            ones++;
        }
        return ones;
    }

    // 3.自己写的low b 循环检查-时间复杂度O(nlogn)-2ms
    int[] countBits3(int n) {
        int bit_max = 31;
        if (n == 0)
            return new int[1];// 默认初始值为0

        while (((n >> bit_max) & 1) == 0)// 找到n的二进制表示有多少位
            --bit_max;
        int[] ans = new int[n + 1];
        for (int i = 0; i <= n; ++i) {
            int ones = 0;
            for (int j = 0; j <= bit_max; ++j)// 每一位逐个比较是否为1
                ones += (i >> j) & 1;// 不用执行判断逻辑（判断&1是否为1 ones+1）
            ans[i] = ones;
        }
        return ans;
    }

    // 4.动态规划——最高有效位（后面两个更好理解）-时间复杂度O(n)
    // 对于正整数 x，如果可以知道最大的正整数 y，使得 y ≤ x 且 y 是 2 的整数次幂，则 y 的二进制表示中只有最高位是 1，其余都是 0
    // 此时称 y 为 x 的「最高有效位」
    int[] countBits4(int n) {
        int[] bits = new int[n + 1];
        int highBit = 0;
        for (int i = 1; i <= n; i++) {
            if ((i & (i - 1)) == 0)// i的二进制表示的最后一个1变成0结果为0，则为形如10..0的最高有效位
                highBit = i;// 动态记录最高有效位 1(1) 10(2) 100(4) 1000(8)
            bits[i] = bits[i - highBit] + 1;
        }
        return bits;
    }

    // 5.动态规划——最低有效位-时间复杂度O(n)
    // 对于正整数 x，将其二进制表示右移一位，等价于将其二进制表示的最低位去掉，得到的数是 x>>1
    // bits[x] = bits[x>>1] + (x&1)（x 除以 2 的余数，x为奇则+1，为偶则不+）
    int[] countBits5(int n) {
        int[] bits = new int[n + 1];
        for (int i = 1; i <= n; i++)
            bits[i] = bits[i >> 1] + (i & 1);
        return bits;
    }

    // 6.动态规划——最低设置位-时间复杂度O(n)
    // 定义正整数 x 的「最低设置位」为 xx 的二进制表示中的最低的 1 所在位
    // x = x & (x − 1)，该运算将 x 的二进制表示的最后一个 1 变成 0
    int[] countBits6(int n) {
        int[] bits = new int[n + 1];
        for (int i = 1; i <= n; i++)
            bits[i] = bits[i & (i - 1)] + 1;
        return bits;
    }

    // 461. 汉明距离
    // 两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
    // 汉明距离-1.异或+java内置函数Integer.bitCount-时间复杂度O(logn)
    int hammingDistance(int x, int y) {
        return Integer.bitCount(x ^ y);// 异或运算
    }

    // 汉明距离-2.移位实现位计数-时间复杂度O(logn)
    int hammingDistance2(int x, int y) {
        int s = x ^ y, ret = 0;
        while (s != 0) {
            ret += s & 1;
            s >>= 1;
        }
        return ret;
    }

    // 汉明距离-3.Brian Kernighan 算法-时间复杂度O(logn)
    int hammingDistance3(int x, int y) {
        int s = x ^ y, ret = 0;
        while (s != 0) {
            s &= s - 1;
            ret++;
        }
        return ret;
    }
}

// 树
class TreeSol {
    // 94. 二叉树的中序遍历
    // 给定一个二叉树的根节点 root ，返回它的 中序 遍历

    // 方法一：递归DFS-空间复杂度O(n)
    // 递归特性：1.节点为空，2.节点不为空
    List<Integer> inorderTraversal(TreeNode root) {

        List<Integer> res = new ArrayList<Integer>();
        inorder(root, res);
        return res;
    }

    void inorder(TreeNode root, List<Integer> res) {
        if (root == null)// 根为空，直接返回
            return;
        inorder(root.left, res);// 左
        res.add(root.val);// 根
        inorder(root.right, res);// 右
    }

    // 方法二：栈（迭代）-空间复杂度O(n)
    // 左根右-对于一个节点：入栈（循环遍历左孩子）、出栈（记录val）、（指针指向出栈节点右孩子）
    List<Integer> inorderTraversal2(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stk = new LinkedList<>();// 速度比Stack快
        while (root != null || !stk.isEmpty()) {// 节点为空指针且栈为空时才跳出
            while (root != null) {// 节点非空
                stk.push(root);// 节点入栈
                root = root.left;// 循环遍历左孩子
            }
            root = stk.pop();// root指向栈顶节点，栈顶节点出栈
            res.add(root.val);// 出栈时记录val
            root = root.right;// 遍历出栈节点右孩子
        }
        return res;
    }

    // 方法三：Morris中序遍历-线索二叉树（找前驱）-空间复杂度O(1)
    // 步骤如下（假设当前遍历到的节点为 x）：
    // 1.如果 x 无左孩子，先将 x 的值加入答案数组，再访问 x 的右孩子。
    // 2.如果 x 有左孩子，则找到 x 左子树上最右的节点（即左子树中序遍历的最后一个节点，x 在中序遍历中的前驱节点）
    // 记为 predecessor。根据 predecessor 的右孩子是否为空，进行如下操作
    // 1.如果 predecessor 的右孩子为空，则将其右孩子指向 x，然后访问 x 的左孩子
    // 2.如果 predecessor 的右孩子不为空，则此时其右孩子指向 x，说明已经遍历完 x 的左子树，
    // 将 predecessor 的右孩子置空，将 x 的值加入答案数组，然后访问 x 的右孩子
    // 3.重复上述操作，直至访问完整棵树
    List<Integer> inorderTraversal3(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        TreeNode predecessor = null;

        while (root != null) {
            if (root.left != null) {
                // predecessor 节点就是当前 root 左子树最右，其右指针必为空
                predecessor = root.left;
                while (predecessor.right != null && predecessor.right != root) {
                    predecessor = predecessor.right;
                }

                // predecessor 的右指针为空，指向 root，继续遍历左子树
                if (predecessor.right == null) {
                    predecessor.right = root;
                    root = root.left;
                }
                // 右指针非空，指向root，即root之前的节点都已遍历
                else {
                    res.add(root.val);
                    predecessor.right = null;
                    root = root.right;
                }
            }
            // 如果没有左孩子，则直接访问右孩子
            else {
                res.add(root.val);
                root = root.right;
            }
        }
        return res;
    }

    // 96. 不同的二叉搜索树
    // 给你一个整数 n ，求恰由 n 个节点组成且节点值从 1 到 n 互不相同的「二叉搜索树」有多少种？返回满足题意的二叉搜索树的种数

    // 方法一：动态规划-时间复杂度O(n2)-空间复杂度O(n)
    // 精髓在于：只考虑对应个数节点的相对位置，不考虑具体的数值
    // 使用递归涉及到大量重复运算，动态规划直接调用最终结果
    // ans = Σ 节点值i的左半边组成二叉搜索树的种数 * 右半边组成二叉搜索树的种数
    int numTrees(int n) {
        // 长度为 n 的序列能构成的不同二叉搜索树的个数1
        // 重点：序列1,2,3 1,3,4能构成的不同二叉搜索树的个数是相同的
        int[] G = new int[n + 1];
        G[0] = 1;// 为方便计算
        G[1] = 1;// 长度为 1 的序列能构成的不同二叉搜索树的个数
        for (int i = 2; i <= n; ++i) {
            for (int j = 0; j <= i - 1; ++j)
                // 节点值i的左半边组成二叉搜索树的种数 * 右半边组成二叉搜索树的种数
                G[i] += G[j] * G[i - 1 - j];
        }
        return G[n];
    }

    // 方法二：数学-时间复杂度O(n)-空间复杂度O(1)
    // G(n)函数的值在数学上被称为卡塔兰数 Cn，卡塔兰数更便于计算的定义如下：
    int numTrees2(int n) {
        // 需要用 long 类型防止计算过程中的溢出!
        long C = 1;
        for (int i = 0; i < n; ++i)
            C = C * 2 * (2 * i + 1) / (i + 2);
        return (int) C;
    }

    // 98. 验证二叉搜索树
    // 给定一个二叉树，判断其是否是一个有效的二叉搜索树
    // 假设一个二叉搜索树具有如下特征：
    // 节点的左子树只包含小于当前节点的数
    // 节点的右子树只包含大于当前节点的数
    // 所有左子树和右子树自身必须也是二叉搜索树
    // 提示：
    // 树中节点数目范围在[1, 104] 内
    // -231 <= Node.val <= 231 - 1 （注意溢出！）

    // 方法一：递归 + 传值（上下界）判断-时空复杂度O(n)
    // 递归特性：①为空，返回；②节点值是否处于上下界中？
    // 维护一个「上下界」
    // 「并不是单纯只满足」：左子树值小于根节点值，右子树值大于根节点值
    boolean isValidBST(TreeNode root) {
        return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    boolean isValidBST(TreeNode node, long lower, long upper) {
        if (node == null)
            return true;

        if (node.val <= lower || node.val >= upper)
            return false;

        return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
        // 可提前return（剪枝）
    }

    // 方法二：中序遍历（迭代）+ 比较值-时空复杂度O(n)
    boolean isValidBST2(TreeNode root) {
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        double inorder = -Double.MAX_VALUE;

        while (!stack.isEmpty() || root != null) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            // 如果中序遍历得到的节点的值小于等于前一个 inorder，说明不是二叉搜索树
            if (root.val <= inorder)
                return false;
            inorder = root.val;
            root = root.right;
        }
        return true;
    }

    // 方法三：中序遍历（递归） + 比较值

    // 101. 对称二叉树
    // 给定一个二叉树，检查它是否是镜像对称的
    // （都重复计算了一遍）

    // 方法一：递归DFS-时间复杂度O(n)
    boolean isSymmetric(TreeNode root) {
        return check(root, root);
    }

    boolean check(TreeNode p, TreeNode q) {
        if (p == null && q == null)
            return true;
        else if (p == null || q == null)
            return false;

        return p.val == q.val && check(p.left, q.right) && check(p.right, q.left);
        // 可通过p.val != q.val 提前跳出
    }

    // 方法二：迭代BFS（队列，null也要入队）-时间复杂度O(n)
    boolean isSymmetric2(TreeNode root) {
        TreeNode u = root, v = root;
        Queue<TreeNode> q = new LinkedList<TreeNode>();
        // 根节点入队
        q.offer(u);
        q.offer(v);
        while (!q.isEmpty()) {
            // 成对出队，前后顺序不能错
            u = q.poll();
            v = q.poll();

            if (u == null && v == null)
                continue;// 进入下一层循环
            if ((u == null || v == null) || (u.val != v.val))
                return false;
            // 成对入队，前后顺序不能错
            // 即使左右子树为null（无需判断）也需要入队，以判断是否对称
            q.offer(u.left);
            q.offer(v.right);

            q.offer(u.right);
            q.offer(v.left);
        }
        return true;
    }

    // 方法三：自己写的迭代DFS（栈）（要改造为null也要入栈，不建议）-时间复杂度O(n)
    boolean isSymmetric3(TreeNode root) {
        TreeNode u = root, v = root;
        Deque<TreeNode> s = new LinkedList<TreeNode>();
        do {
            while (u != null && v != null) {
                // 成对入队
                s.push(u);
                s.push(v);
                u = u.left;
                v = v.right;
            }
            // 不同时为null
            if ((u == null) != (v == null))
                return false;
            // 成对出队，前后顺序不能错
            v = s.pop();
            u = s.pop();
            // u v不一样，或者值不相同
            if ((u == null) != (v == null))
                return false;
            else if (u.val != v.val)
                return false;
            u = u.right;
            v = v.left;
        } while (!s.isEmpty());// 栈为空时，u v第二次到达root节点，已对比完毕
        return true;
    }

    // 102. 二叉树的层序遍历
    // 给你一个二叉树，请你返回其按「层序遍历」得到的节点值。 （即逐层地，从左到右访问所有节点）

    // 方法一：BFS-多元素拓展-O(n)
    // 和普通广度优先搜索的区别在于：
    // 普通广度优先搜索每次只取一个元素拓展，而这里每次取i个元素。在上述过程中的第 i 次迭代就得到了二叉树的第 i 层的 i个元素
    List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        if (root == null)
            return ret;

        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);// 根节点入队
        while (!queue.isEmpty()) {
            List<Integer> level = new ArrayList<Integer>();// 每一层
            int currentLevelSize = queue.size();// 获取当前层大小
            for (int i = 1; i <= currentLevelSize; ++i) {
                TreeNode node = queue.poll();
                level.add(node.val);
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
            ret.add(level);
        }

        return ret;
    }

    // 方法二：BFS-自己写的lowb双队列-O(n)
    List<List<Integer>> levelOrder2(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        if (root == null)
            return ret;

        Queue<TreeNode> q1 = new LinkedList<TreeNode>();
        Queue<TreeNode> q2 = new LinkedList<TreeNode>();
        List<Integer> level = new ArrayList<Integer>();// 每一层
        q1.offer(root);// 根节点入队
        Queue<TreeNode> qout = q1, qin = q2;

        while (!qin.isEmpty() || !qout.isEmpty()) {
            TreeNode node = qout.poll();// 出队
            level.add(node.val);
            if (node.left != null)
                qin.offer(node.left);
            if (node.right != null)
                qin.offer(node.right);

            if (qout.isEmpty()) {// 最后一层遍历结束时，两个队列都为空，写入ret，再while判断退出
                // 交换qin qout
                qout = (qout == q1) ? q2 : q1;
                qin = (qin == q1) ? q2 : q1;
                ret.add(level);
                level = new ArrayList<Integer>();// 不能用clear，要new一个List
            }
        }
        return ret;
    }

    // 104. 二叉树的最大深度
    // 给定一个二叉树，找出其最大深度
    // 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数
    // 说明: 叶子节点是指没有子节点的节点

    // 方法一：DFS-空间复杂度-O(h)
    int maxDepth(TreeNode root) {// 三元运算符比if-else更快
        return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;// 省去创建局部变量
    }

    // 方法二：BFS-空间复杂度-O(n)
    int maxDepth2(TreeNode root) {
        if (root == null)
            return 0;
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        int ans = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
            ans++;
        }
        return ans;
    }

    // 105. 从前序与中序遍历序列构造二叉树
    // 给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
    // 1 <= preorder.length <= 3000
    // inorder.length == preorder.length
    // -3000 <= preorder[i], inorder[i] <= 3000
    // preorder 和 inorder 「均无重复元素」
    // inorder 均出现在 preorder
    // preorder 保证为二叉树的前序遍历序列
    // inorder 保证为二叉树的中序遍历序列

    // 方法一：递归-时间复杂度、空间复杂度：O(n)
    // 无重复元素，则可用哈希表
    // 递归时，需要传入前序、中序的左右边界
    Map<Integer, Integer> indexMap;

    TreeNode buildTree(int[] preorder, int[] inorder) {
        int n = preorder.length;
        // 构造哈希映射，值——→索引，以便快速定位中序遍历中对应根节点的索引值，而前序遍历的根节点一定是区间内第一个节点
        indexMap = new HashMap<Integer, Integer>();
        for (int i = 0; i < n; i++)
            indexMap.put(inorder[i], i);
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }

    // ========================前序遍历========中序遍历=====前序遍历索引的左区间，右区间============中序遍历索引的左区间，右区间
    TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left,
            int inorder_right) {
        // 前序遍历的形式:[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]，即根节点总是前序遍历中的第一个节点
        // 中序遍历的形式:[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
        // 前序遍历的左区间用来确定根节点，右区间用来确定是否有子树，没有则提前返回null
        // 中序遍历的根节点由前序遍历的左区间确定，左区间确定左子树的节点数目
        if (preorder_left > preorder_right)// 单独使用前序遍历，或中序遍历的区间范围来判断都可行
            return null;

        int preorder_root = preorder_left;// 前序遍历中的第一个节点就是根节点
        int inorder_root = indexMap.get(preorder[preorder_root]);// 在中序遍历中定位根节点
        TreeNode root = new TreeNode(preorder[preorder_root]); // 先把根节点建立出来
        int size_left_subtree = inorder_root - inorder_left;// 得到左子树中的节点数目
        // 递归地构造左右子树，并连接到根节点
        // 先序遍历左子树边界[左边界+1, 左边界+size_left_subtree]，中序遍历左子树边界[左边界, 根节点定位-1]
        root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left,
                inorder_root - 1);
        // 先序遍历右子树边界「左边界+1+左子树节点数目, 右边界」，中序遍历右子树边界「根节点定位+1, 右边界」
        root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right,
                inorder_root + 1, inorder_right);
        return root;
    }

    // 方法二：迭代-时间复杂度、空间复杂度：O(n)
    // 对于前序遍历中的任意两个连续节点 u 和 v，根据前序遍历的流程，可知 u 和 v 只有两种可能的关系：
    // 1. v 是 u 的左孩子。这是因为在遍历到 u 之后，下一个遍历的节点就是 u 的左孩子，即 v
    // 2. u 没有左孩子，并且 v 是 u 的某个祖先节点（或者 u 本身）的右孩子。如果 u 没有左孩子，那么下一个遍历的节点就是 u 的右孩子
    // 如果 u 没有右孩子，我们就会向上回溯，直到遇到第一个有右孩子（且 u 不在它的右孩子的子树中）的节点 u_a，那么 v 就是 u_a 的右孩子
    TreeNode buildTree2(int[] preorder, int[] inorder) {
        if (preorder == null) // 空树提前返回null
            return null;
        TreeNode root = new TreeNode(preorder[0]);
        // 用一个栈 stack 来维护「当前节点的所有还没有考虑过右孩子的祖先节点」，栈顶就是当前节点。也就是说，只有在栈中的节点才可能连接一个新的右孩子
        // 有点类似于中序遍历的迭代中的栈
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        stack.push(root);// 根节点入栈
        int inorderIndex = 0;// 指向中序遍历的某个位置，当前节点不断往左走达到的最终节点
        for (int i = 1; i < preorder.length; i++) {
            int preorderVal = preorder[i];// 例子中的节点v，根节点是节点u
            TreeNode node = stack.peek();// 根节点出栈
            if (node.val != inorder[inorderIndex]) {// 情况1. 节点v是u的左孩子
                // 若inorderIndex已是不断往左走达到的最终节点（节点u没有左孩子），则进入情况2.
                node.left = new TreeNode(preorderVal);
                stack.push(node.left);// 没有考虑过右孩子的节点通通入栈
            } else {// 情况2. u 没有左孩子，向上回溯
                while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
                    node = stack.pop();
                    inorderIndex++;
                }
                node.right = new TreeNode(preorderVal);
                stack.push(node.right);// 没有考虑过右孩子的节点通通入栈
            }
        }
        return root;
    }

    // 124. 二叉树中的最大路径和
    // 路径 被定义为一条从树中任意节点出发，沿父节点-子节点连接，达到任意节点的序列
    // 同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点，且不一定经过根节点
    // 「路径和」是路径中各节点值的总和
    // 给你一个二叉树的根节点 root ，返回其「最大路径和」
    // 树中节点数目范围是 [1, 3e4]
    // -1000 <= Node.val <= 1000

    // 方法一：dfs 递归 后序-时间复杂度、空间复杂度：O(n)
    int maxSum = Integer.MIN_VALUE;

    int maxPathSum(TreeNode root) {
        maxGain(root);
        return maxSum;
    }

    int maxGain(TreeNode node) {
        if (node == null)
            return 0;

        // 递归计算左右子节点的最大贡献值，只有在最大贡献值大于 0 时，才会选取对应子节点
        // 贡献为负，则不要子节点的贡献，取0（精髓之一，非常巧妙）
        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);
        int priceNewpath = node.val + leftGain + rightGain;// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        maxSum = Math.max(maxSum, priceNewpath); // 更新答案
        return node.val + Math.max(leftGain, rightGain);// 返回节点的最大贡献值（这里可能为负）
    }

    // 226. 翻转二叉树
    // 翻转二叉树-1.递归DFS（后序）-时间复杂度、空间复杂度：O(n)
    TreeNode invertTree(TreeNode root) {
        if (root == null)
            return null;

        TreeNode left = invertTree(root.left);// 翻转root节点左子树的左右子树
        TreeNode right = invertTree(root.right);// 翻转root节点右子树的左右子树
        root.left = right;// 翻转root节点的左右子树
        root.right = left;
        return root;
    }

    // 翻转二叉树-2.自己写的lowb迭代BFS-时间复杂度、空间复杂度：O(n)
    TreeNode invertTree2(TreeNode root) {
        if (root == null)
            return null;
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode node = q.poll();
            TreeNode temp = node.left;
            if (node.left != null)
                q.offer(node.left);
            if (node.right != null)
                q.offer(node.right);
            node.left = node.right;
            node.right = temp;
        }
        return root;
    }

    // 236. 二叉树的最近公共祖先
    // 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先
    // 公共祖先的定义为：“对于有根树 T 的两个节点 p、q，最近公共祖先表示为一个节点 x
    // 满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）
    // 树中节点数目在范围 [2, 105] 内
    // -109 <= Node.val <= 109
    // 所有 Node.val 「互不相同」
    // p != q
    // p 和 q 均存在于给定的二叉树中

    // 方法一：递归DFS（后序遍历）-时间复杂度，空间复杂度-O(n)
    // 递归遍历整棵二叉树，定义 fx 表示 x 节点的子树中是否包含 p 节点或 q 节点，如果包含为 true，否则为 false
    // 那么符合条件的最近公共祖先 x 一定满足如下条件：
    // (flson && frson) ||  ((x = p || x = q) && (flson || frson))
    // flson && frson一定符合
    // (x = p || x = q) && (flson || frson)同时满足也符合，即父节点是p或q且另一个在其左或右子树下
    TreeNode ans = null;

    boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null)
            return false;
        boolean lson = dfs(root.left, p, q);// p q 是否在左子树下
        boolean rson = dfs(root.right, p, q);// p q 是否在右子树下
        if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson)))
            ans = root;

        return lson || rson || (root.val == p.val || root.val == q.val);// p q是否在该节点下，或该节点就是p q
    }

    TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        this.dfs(root, p, q);
        return ans;
    }

    // 方法二：存储父节点-时间复杂度，空间复杂度-O(n)
    // 不是顺序二叉树，不能简单通过序号来判断是否是父节点
    Map<Integer, TreeNode> parent = new HashMap<Integer, TreeNode>();// 键值对，key为节点val，value为该节点的父节点
    Set<Integer> visited = new HashSet<Integer>();// 所有节点p的祖先节点（包括p自身）

    void dfs(TreeNode root) {
        if (root.left != null) {
            parent.put(root.left.val, root);
            dfs(root.left);
        }
        if (root.right != null) {
            parent.put(root.right.val, root);
            dfs(root.right);
        }
    }

    TreeNode lowestCommonAncestor2(TreeNode root, TreeNode p, TreeNode q) {
        dfs(root);
        while (p != null) {
            visited.add(p.val);// p的所有父节点，包括p节点自己
            p = parent.get(p.val);// p的父节点
        }
        while (q != null) {
            if (visited.contains(q.val))// 找到共同的祖先节点
                return q;
            q = parent.get(q.val);
        }
        return null;
    }

    // 297. 二叉树的序列化与反序列化
    // 序列化是将一个数据结构或者对象转换为连续的比特位的操作，进而可以将转换后的数据存储在一个文件或者内存中
    // 同时也可以通过网络传输到另一个计算机环境，采取相反方式重构得到原数据
    // 请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列（自己设计序列）
    // 反序列化算法执行逻辑，你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构
    // 树中节点数在范围 [0, 104] 内
    // -1000 <= Node.val <= 1000
    // 题目要求：
    // 编解码器对象将被实例化并按此方式调用
    // Codec ser = new Codec();
    // Codec deser = new Codec();
    // TreeNode ans = deser.deserialize(ser.serialize(root));

    // 方法一：DFS（先序遍历）-时间复杂度、空间复杂度：O(n)
    // 先序遍历，String用,隔开，null用None表示，再按先序遍历反序列化
    // 含有所有null的先序遍历，是可以还原出树的结构的
    // String类型是引用数据类型，但是因为有字符串常量池，做参数是值的复制
    // StringBuilder是引用数据类型，做参数是传递地址
    String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder("");
        rserialize(root, sb);
        return new String(sb);
    }

    void rserialize(TreeNode root, StringBuilder str) {
        if (root == null)
            str.append("None,");
        else {
            str.append(String.valueOf(root.val) + ",");// 先序遍历
            rserialize(root.left, str);
            rserialize(root.right, str);
        }

    }

    TreeNode deserialize(String data) {
        String[] dataArray = data.split(",");

        // 数组转List，如果就要使用数组，需要传入index指向当前节点位置，但是返回值已经有TreeNode，最好不这样
        // Arrays.asList() 得到的是内部类，需要放入容器的构造器中，再进行使用！
        Queue<String> dataList = new LinkedList<>(Arrays.asList(dataArray));
        return rdeserialize(dataList);
    }

    TreeNode rdeserialize(Queue<String> dataList) {
        if ("None".equals(dataList.peek())) {
            dataList.poll();
            return null;
        }
        // 按先序遍历反序列化
        TreeNode root = new TreeNode(Integer.parseInt(dataList.peek()));
        dataList.poll();
        root.left = rdeserialize(dataList);
        root.right = rdeserialize(dataList);
        return root;
    }

    // 方法二：括号表示编码（中序遍历）+ 递归下降解码
    // 时间复杂度、空间复杂度：O(n)
    // 也可以这样表示一颗二叉树：
    // 如果当前的树为空，则表示为 X
    // 如果当前的树不为空，则表示为 (左子树序列化之后的结果)当前节点val(右子树序列化之后的结果)
    // 根据这样的定义，很好写出序列化的过程，「后序遍历」这颗二叉树即可，像方法1那样从左至右序列化不行
    // 那如何反序列化呢？根据定义，我们可以推导出这样的巴科斯范式（BNF）：
    // T -> (T) num (T) | X
    // 它的意义是：用 T 代表一棵树序列化之后的结果，| 表示 T 的构成为 (T) num (T) 或者 X
    // | 左边是对 T的递归定义，右边规定了递归终止的边界条件

    String serialize2(TreeNode root) {
        return rserialize2(root);
    }

    String rserialize2(TreeNode root) {
        if (root == null)
            return "X";

        return "(" + rserialize2(root.left) + ")" + root.val + "(" + rserialize2(root.right) + ")";
    }

    TreeNode deserialize2(String data) {
        int[] ptr = { 0 };// 为了不创建类的成员变量，（基本数据类型传入数据的复制，引用数据类型传入地址），用包装类也不行
        return parse(data, ptr);
    }

    TreeNode parse(String data, int[] ptr) {
        if (data.charAt(ptr[0]) == 'X') {
            ++ptr[0];
            return null;
        }
        TreeNode cur = new TreeNode(0);
        ++ptr[0]; // 跳过左括号
        cur.left = parse(data, ptr);// 左子树
        ++ptr[0]; // 跳过右括号

        cur.val = parseInt(data, ptr);// 当前节点值

        ++ptr[0]; // 跳过左括号
        cur.right = parse(data, ptr);// 右子树
        ++ptr[0]; // 跳过右括号
        return cur;
    }

    int parseInt(String data, int[] ptr) {
        int x = 0, sgn = 1;// 节点值有正负，-1000 <= Node.val <= 1000
        if (!Character.isDigit(data.charAt(ptr[0]))) {// 数字第一位是-
            sgn = -1;
            ++ptr[0];
        }
        while (Character.isDigit(data.charAt(ptr[0])))
            x = x * 10 + data.charAt(ptr[0]++) - '0';

        return x * sgn;
    }

    // 337. 打家劫舍 III
    // 在上次打劫完一条街道之后和一圈房屋后，小偷又发现了一个新的可行窃的地区。这个地区只有一个入口，我们称之为“根”
    // 除了“根”之外，每栋房子有且只有一个“父“房子与之相连。一番侦察之后，聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”
    // 如果两个直接相连的房子在同一天晚上被打劫，房屋将自动报警
    // 计算在不触动警报的情况下，小偷一晚能够盗取的最高金额

    // 打家劫舍 III-1.动态规划
    // 用 f(o) 表示选择 o 节点的情况下，o 节点的子树上被选择的节点的最大权值和
    // g(o) 表示不选择 o 节点的情况下， o 节点的子树上被选择的节点的最大权值和
    // l 和 r 代表 o 的左右孩子
    // 1.当 o 被选中时，o 的左右孩子都不能被选中，故 o 被选中情况下子树上被选中点的最大权值和为 l 和 r 不被选中的最大权值和相加
    // 即 f(o)=g(l)+g(r)
    // 2.当 o 不被选中时，o 的左右孩子可以被选中，也可以不被选中。对于 o 的某个具体的孩子 x，它对 o 的贡献是 x
    // 被选中和不被选中情况下权值和的较大值
    // 故 g(o)=max{f(l),g(l)}+max{f(r),g(r)}
    public int rob(TreeNode root) {
        int[] rootStatus = robDFS(root);
        return Math.max(rootStatus[0], rootStatus[1]);
    }

    public int[] robDFS(TreeNode node) {
        if (node == null)
            return new int[] { 0, 0 };

        int[] l = robDFS(node.left);
        int[] r = robDFS(node.right);
        int selected = node.val + l[1] + r[1];// 1.
        int notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);// 2.
        return new int[] { selected, notSelected };
    }

    // 437. 路径总和 III
    // 给定一个二叉树的根节点 root ，和一个整数 targetSum ，求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
    // 路径 不需要从根节点开始，也不需要在叶子节点结束，但是「路径方向必须是向下」的（只能从父节点到子节点）。
    // 二叉树的节点个数的范围是 [0,1000]
    // -109 <= Node.val <= 109
    // -1000 <= targetSum <= 1000
    // 对每个节点进行dfs，时间复杂度太高

    // 方法一：前缀和 dfs、回溯（自顶向下）-时间复杂度、空间复杂度：O(n)
    // 其实就是作差的思想
    // 在同一个路径之下（可以理解成二叉树从root节点出发，到叶子节点的某一条路径），
    // 如果前缀总和currSum，在节点A和节点B处相差target，则位于节点A和节点B之间的元素之和是target。

    int target;
    Map<Integer, Integer> prefixSumCount = new HashMap<>();// key是前缀和, value是大小为key的前缀和出现的次数

    public int pathSum(TreeNode root, int sum) {
        target = sum;
        prefixSumCount.put(0, 1);// 精髓之一：前缀和为0的一条路径
        return recursionPathSum(root, 0);// 前缀和的递归回溯思路
    }

    /**
     * 前缀和的递归回溯思路 从当前节点反推到根节点(反推比较好理解，正向其实也只有一条)，有且仅有一条路径，因为这是一棵树
     * 如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
     * 所以前缀和对于当前路径来说是唯一的，当前记录的前缀和，在回溯结束，回到本层时去除，保证其不影响其他分支的结果
     * 
     * @param node           树节点
     * @param prefixSumCount 前缀和Map
     * @param target         目标值
     * @param currSum        当前路径和
     * @return 满足题意的解
     */
    private int recursionPathSum(TreeNode node, int currSum) {
        // 1.递归终止条件
        if (node == null)
            return 0;
        // 2.本层要做的事情
        int res = 0;

        currSum += node.val;// 当前路径上的和

        // ---核心代码
        // 看看root到当前节点这条路上是否存在节点前缀和加target为currSum的路径
        // 当前节点->root节点反推，有且仅有一条路径，「如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了」
        // currSum-target相当于找路径的起点，起点的sum+target=currSum，当前点到起点的距离就是target
        res += prefixSumCount.getOrDefault(currSum - target, 0);// 路径数
        prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);// 更新路径上当前节点前缀和的个数
        // ---核心代码

        // 3.进入下一层
        res += recursionPathSum(node.left, currSum);
        res += recursionPathSum(node.right, currSum);

        // 4.回到本层，恢复状态，去除当前节点的前缀和数量（DFS离开当前节点）
        prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);
        return res;
    }

    // 538. 把二叉搜索树转换为累加树
    // 给出二叉 搜索 树的根节点，该树的节点值各不相同，请你将其转换为累加树（Greater Sum Tree），使每个节点
    // node 的新值等于原树中大于或等于 node.val 的值之和。
    // 提醒一下，二叉搜索树满足下列约束条件：
    // 节点的左子树仅包含键 小于 节点键的节点。
    // 节点的右子树仅包含键 大于 节点键的节点。
    // 左右子树也必须是二叉搜索树。

    // 方法一：反序中序遍历-空间复杂度-O(n)
    int sum = 0;

    public TreeNode convertBST(TreeNode root) {
        if (root != null) {
            convertBST(root.right);// 先右
            sum += root.val;// 加后赋值
            root.val = sum;
            convertBST(root.left);// 后左
        }
        return root;
    }

    // 方法二：Morris 遍历-空间复杂度-O(1)
    // Morris 遍历的核心思想是利用树的大量空闲指针，实现空间开销的极限缩减。其「反序中序遍历」规则总结如下：
    // 1.如果当前节点的右子节点为空，处理当前节点，并遍历当前节点的左子节点；
    // 2.如果当前节点的右子节点不为空，找到当前节点右子树的最左节点（该节点为当前节点中序遍历的前驱节点）；
    // 如果最左节点的左指针为空，将最左节点的左指针指向当前节点，遍历当前节点的右子节点；
    // 如果最左节点的左指针不为空，将最左节点的左指针重新置为空（恢复树的原状），处理当前节点，并将当前节点置为其左节点；
    // 3.重复步骤 1 和步骤 2，直到遍历结束。
    public TreeNode convertBST2(TreeNode root) {
        int sum = 0;
        TreeNode node = root;

        while (node != null) {
            if (node.right == null) {
                sum += node.val;
                node.val = sum;
                node = node.left;
            } else {
                TreeNode succ = getSuccessor(node);
                if (succ.left == null) {
                    succ.left = node;
                    node = node.right;
                } else {
                    succ.left = null;
                    sum += node.val;
                    node.val = sum;
                    node = node.left;
                }
            }
        }

        return root;
    }

    public TreeNode getSuccessor(TreeNode node) {
        TreeNode succ = node.right;
        while (succ.left != null && succ.left != node)
            succ = succ.left;

        return succ;
    }

    // 543. 二叉树的直径
    // 给定一棵二叉树，你需要计算它的直径长度。一棵二叉树的直径长度是任意两个节点路径长度中的最大值。这条路径可能穿过也可能不穿过根节点。
    // 1.dfs（后序遍历）-时间复杂度、空间复杂度-O(n)
    int maxD = 0;

    public int diameterOfBinaryTree(TreeNode root) {
        depth(root);
        return maxD;
    }

    public int depth(TreeNode node) {
        if (node == null)
            return 0;

        int Left = depth(node.left);
        int Right = depth(node.right);
        maxD = Math.max(Left + Right, maxD);// 将每个节点最大直径(左子树深度+右子树深度)当前最大值比较并取大者
        return Math.max(Left, Right) + 1;// 返回节点深度
    }

    // 617. 合并二叉树
    // 给定两个二叉树，想象当你将它们中的一个覆盖到另一个上时，两个二叉树的一些节点便会重叠。
    // 你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠，那么将他们的值相加作为节点合并后的新值，
    // 否则不为 NULL 的节点将直接作为新二叉树的节点。
    // 注意: 合并必须从两个树的根节点开始。

    // 1.dfs（先序遍历）-时间复杂度、空间复杂度-O(min(n1,n2))
    public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
        if (t1 == null)
            return t2;

        if (t2 == null)
            return t1;

        TreeNode merged = new TreeNode(t1.val + t2.val);
        merged.left = mergeTrees(t1.left, t2.left);
        merged.right = mergeTrees(t1.right, t2.right);
        return merged;
    }

    // 2.bfs（三个辅助队列）-时间复杂度、空间复杂度-O(min(n1,n2))
    // 这么长，没必要了
    public TreeNode mergeTrees2(TreeNode t1, TreeNode t2) {
        if (t1 == null)
            return t2;

        if (t2 == null)
            return t1;

        TreeNode merged = new TreeNode(t1.val + t2.val);// 根节点
        // 加入队列的节点一定是合并的节点
        Queue<TreeNode> queue = new LinkedList<TreeNode>();// 存放merged节点
        Queue<TreeNode> queue1 = new LinkedList<TreeNode>();// 存放t1的节点
        Queue<TreeNode> queue2 = new LinkedList<TreeNode>();// 存放t2的节点
        queue.offer(merged);
        queue1.offer(t1);
        queue2.offer(t2);// 入队
        while (!queue1.isEmpty() && !queue2.isEmpty()) {
            TreeNode node = queue.poll(), node1 = queue1.poll(), node2 = queue2.poll();// 出队
            TreeNode left1 = node1.left, left2 = node2.left, right1 = node1.right, right2 = node2.right;
            if (left1 != null || left2 != null) {// t1左子树或t2左子树不同时为空
                if (left1 != null && left2 != null) {// 只有在两树左子树都不为空的情况下才需要三队列入队
                    TreeNode left = new TreeNode(left1.val + left2.val);
                    node.left = left;
                    queue.offer(left);
                    queue1.offer(left1);
                    queue2.offer(left2);
                } else if (left1 != null) {// 其中一个树的左子树为空，则直接令merged树指向另一个不为空的左子树即可
                    node.left = left1;// 这个节点就不加入队列了
                } else if (left2 != null) // 同上
                    node.left = left2;

            }
            if (right1 != null || right2 != null) {// t1右子树或t2右子树不同时为空
                if (right1 != null && right2 != null) {
                    TreeNode right = new TreeNode(right1.val + right2.val);
                    node.right = right;
                    queue.offer(right);
                    queue1.offer(right1);
                    queue2.offer(right2);
                } else if (right1 != null) {
                    node.right = right1;
                } else {
                    node.right = right2;
                }
            }
            // t1 t2的左右子树同时为空时，不处理即可
        }
        return merged;
    }

}

// 深度优先遍历
class DFSSol {
    // 94. 二叉树的中序遍历
    // 98. 验证二叉搜索树
    // 101. 对称二叉树
    // 104. 二叉树的最大深度
    // 114. 二叉树展开为链表
    // 124. 二叉树中的最大路径和

    // 200. 岛屿数量
    // 给你一个由 '1'（陆地）和 '0'（水）组成的的二维网格，请你计算网格中岛屿的数量。
    // 岛屿总是被水包围，并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
    // 此外，你可以假设该网格的四条边均被水包围。
    // 提示：
    // m == grid.length
    // n == grid[i].length
    // 1 <= m, n <= 300
    // grid[i][j] 的值为 '0' 或 '1'

    // 方法一：dfs-时间复杂度、空间复杂度-O(mn)
    // 类似于图的dfs遍历，使用visited数组记录被访问过的节点，循环遍历每个未被访问过的节点，直到所有连通分支被访问
    // 该方法的优化点：只有1会访问，被访问过的点置为0，则之后的循环不会再访问该点，省去使用visited数组的空间
    void dfs(char[][] grid, int r, int c) {
        int m = grid.length;
        int n = grid[0].length;

        if (r < 0 || c < 0 || r >= m || c >= n || grid[r][c] == '0') // 为边界外，或为0
            return;

        grid[r][c] = '0';
        dfs(grid, r - 1, c);// 上
        dfs(grid, r + 1, c);// 下
        dfs(grid, r, c - 1);// 左
        dfs(grid, r, c + 1);// 右
    }

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0)
            return 0;

        int m = grid.length;
        int n = grid[0].length;
        int num_islands = 0;
        for (int r = 0; r < m; ++r)
            for (int c = 0; c < n; ++c)
                if (grid[r][c] == '1') {
                    ++num_islands;
                    dfs(grid, r, c);
                }

        return num_islands;
    }

    // 方法二：bfs-时间复杂度：O(mn)，空间复杂度-O(min(m, n))
    // 同样省去使用visited数组
    // 使用bfs时一定要入队时就标记已访问
    public int numIslands2(char[][] grid) {
        if (grid == null || grid.length == 0)
            return 0;

        int m = grid.length;
        int n = grid[0].length;
        int num_islands = 0;

        for (int r = 0; r < m; ++r)// 上下边界
            for (int c = 0; c < n; ++c)// 左右边界
                if (grid[r][c] == '1') {
                    ++num_islands;
                    grid[r][c] = '0';
                    Queue<Integer> neighbors = new LinkedList<>();// 每个“连通分量”都使用一个辅助队列进行bfs
                    neighbors.add(r * n + c);// 将二维坐标转换成一维，入队
                    while (!neighbors.isEmpty()) {
                        int id = neighbors.remove();
                        int row = id / n;// 一维坐标转化为二维
                        int col = id % n;
                        if (row - 1 >= 0 && grid[row - 1][col] == '1') {// 其上为1则加入队列
                            neighbors.add((row - 1) * n + col);
                            grid[row - 1][col] = '0';
                        }
                        if (row + 1 < m && grid[row + 1][col] == '1') {// 下
                            neighbors.add((row + 1) * n + col);
                            grid[row + 1][col] = '0';
                        }
                        if (col - 1 >= 0 && grid[row][col - 1] == '1') {// 左
                            neighbors.add(row * n + col - 1);
                            grid[row][col - 1] = '0';
                        }
                        if (col + 1 < n && grid[row][col + 1] == '1') {// 右
                            neighbors.add(row * n + col + 1);
                            grid[row][col + 1] = '0';
                        }
                    }
                }

        return num_islands;
    }

    // 方法二：自己写的bfs-时间复杂度：O(mn)，空间复杂度-O(min(m, n))
    public int numIslands22(char[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int res = 0;
        Queue<int[]> queue = new LinkedList<>();
        int directions[][] = new int[][] { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '0')
                    continue;

                res++;
                queue.offer(new int[] { i, j });
                grid[i][j] = '0';
                while (!queue.isEmpty()) {
                    int[] node = queue.poll();
                    int node_i = node[0];
                    int node_j = node[1];
                    for (int[] direction : directions) {
                        int new_i = node_i + direction[0];
                        int new_j = node_j + direction[1];
                        if (0 <= new_i && new_i < m && 0 <= new_j && new_j < n && grid[new_i][new_j] == '1') {
                            queue.offer(new int[] { new_i, new_j });
                            grid[new_i][new_j] = '0';
                        }
                    }
                }
            }
        }
        return res;
    }

    // 方法三：并查集-时间复杂度：O(mn*α(mn))，空间复杂度：O(mn)
    // 当使用路径压缩（见 find 函数）和按秩合并（见数组 rank）实现并查集时，单次操作的时间复杂度为 α(mn)，
    // 其中 α(x) 为反阿克曼函数，当自变量 x 的值在人类可观测的范围内（宇宙中粒子的数量）时，函数 α(x) 的值不会超过 5，
    // 因此也可以看成是常数时间复杂度
    // 使用并查集代替搜索：
    // 为了求出岛屿的数量，我们可以扫描整个二维网格。如果一个位置为 1，则将其与相邻四个方向上的 1 在并查集中进行合并。
    // 最终岛屿的数量就是并查集中连通分量的数目。
    class UnionFind {// 内部类
        int count;// 连通分量的数目
        int[] parent;// 每个连通分量中的所有节点的最终parent，都为同一个（该连通分量的第一个被访问的节点）
        int[] rank;// 根据rank决定合并时x, y哪个节点作为“父节点”

        public UnionFind(char[][] grid) {// 构造函数初始化
            count = 0;
            int m = grid.length;
            int n = grid[0].length;
            parent = new int[m * n];
            rank = new int[m * n];
            for (int i = 0; i < m; ++i)
                for (int j = 0; j < n; ++j) {// 二维坐标转一维
                    if (grid[i][j] == '1') {
                        parent[i * n + j] = i * n + j;
                        ++count;// 只要为1就++，开始合并后--
                    }
                    rank[i * n + j] = 0;
                }

        }

        public int find(int i) {// 路径压缩，寻找节点i的“父节点”
            if (parent[i] != i)
                parent[i] = find(parent[i]);// 修改父节点指向，指向父节点的父节点
            return parent[i];
        }

        public void union(int x, int y) {// 按秩合并， x, y为节点坐标，x 与 y 在并查集中进行合并
            int rootx = find(x);
            int rooty = find(y);
            if (rootx != rooty) {// root一样则两个点已经合并过了，不一样则合并，根据rank大小决定谁是“父节点”
                if (rank[rootx] > rank[rooty])
                    parent[rooty] = rootx;
                else if (rank[rootx] < rank[rooty])
                    parent[rootx] = rooty;
                else {// x, y节点rank相等，x的rank++
                    parent[rooty] = rootx;
                    rank[rootx] += 1;
                }
                --count;
            }
        }

        public int getCount() {
            return count;
        }
    }

    public int numIslands3(char[][] grid) {
        if (grid == null || grid.length == 0)
            return 0;

        int m = grid.length;
        int n = grid[0].length;
        UnionFind uf = new UnionFind(grid);
        for (int r = 0; r < m; ++r) // 上下边界
            for (int c = 0; c < n; ++c) // 左右边界
                if (grid[r][c] == '1') {// 如果一个位置为 1，则将其与相邻四个方向上的 1 在并查集中进行合并
                    grid[r][c] = '0';// 访问过的置为0
                    if (r - 1 >= 0 && grid[r - 1][c] == '1')// 上
                        uf.union(r * n + c, (r - 1) * n + c);

                    if (r + 1 < m && grid[r + 1][c] == '1')// 下
                        uf.union(r * n + c, (r + 1) * n + c);

                    if (c - 1 >= 0 && grid[r][c - 1] == '1')// 左
                        uf.union(r * n + c, r * n + c - 1);

                    if (c + 1 < n && grid[r][c + 1] == '1')// 右
                        uf.union(r * n + c, r * n + c + 1);

                }

        return uf.getCount();
    }

    // 207. 课程表（是否存在拓扑排序）
    // 你这个学期必须选修 numCourses 门课程，记为 0 到 numCourses - 1 。
    // 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出，其中 prerequisites[i] = [ai, bi] ，
    // 表示如果要学习课程 ai 则 必须 先学习课程  bi 。
    // 例如，先修课程对 [0, 1] 表示：想要学习课程 0 ，你需要先完成课程 1 。
    // 请你判断是否可能完成所有课程的学习？如果可以，返回 true ；否则，返回 false 。
    // 提示：
    // 1 <= numCourses <= 1e5
    // 0 <= prerequisites.length <= 5000
    // prerequisites[i].length == 2
    // 0 <= ai, bi < numCourses
    // prerequisites[i] 中的所有课程对 互不相同
    // 「判断图 G 是否存在拓扑排序」，至少也要对其进行一次完整的遍历，时间复杂度也为 O(n+m)
    // 因此不可能存在一种仅判断图是否存在拓扑排序的方法。它的时间复杂度在渐进意义上严格优于 O(n+m)

    // 方法一：bfs（辅助队列）（判据：是否访问到了所有节点，有环必不能访问到所有节点）-时间复杂度、空间复杂度-O(n+m)
    // 我们使用一个队列来进行广度优先搜索。初始时，所有入度为 0 的节点都被放入队列中，
    // 它们就是可以作为拓扑排序最前面的节点，并且它们之间的相对顺序是无关紧要的。
    // 在广度优先搜索的每一步中，我们取出队首的节点 u：
    // 我们将 u 放入答案中；
    // 我们移除 u 的所有出边，也就是将 u 的所有相邻节点的入度减少 1。如果某个相邻节点 v 的入度变为 0，那么我们就将 v 放入队列中。
    // 在广度优先搜索的过程结束后。如果答案中包含了这 n 个节点，那么我们就找到了一种拓扑排序，否则说明图中存在环，也就不存在拓扑排序了。
    // 优化：
    // 由于我们只需要判断是否存在一种拓扑排序，因此我们省去存放答案数组，而是只用一个变量记录被放入答案数组的节点个数。
    // 在广度优先搜索结束之后，我们判断该变量的值是否等于课程数，就能知道是否存在一种拓扑排序。

    List<List<Integer>> edges2;// 存放后继节点
    int[] indeg;// 入度

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        edges2 = new ArrayList<List<Integer>>();// 不定长顺序表
        for (int i = 0; i < numCourses; ++i)
            edges2.add(new ArrayList<Integer>());

        indeg = new int[numCourses];
        for (int[] info : prerequisites) {
            edges2.get(info[1]).add(info[0]);// 存放后继节点
            ++indeg[info[0]];// 后继节点入度+1
        }

        Queue<Integer> queue = new LinkedList<Integer>();// 辅助队列
        for (int i = 0; i < numCourses; ++i)
            if (indeg[i] == 0)// 入度为0的入队
                queue.offer(i);

        int visited = 0;
        while (!queue.isEmpty()) {
            ++visited;
            int u = queue.poll();// 出队
            for (int v : edges2.get(u)) {
                --indeg[v];// 后继节点入度-1
                if (indeg[v] == 0) // 入队为0则入队
                    queue.offer(v);
            }
        }

        return visited == numCourses;// 所有节点均被访问过，则无环，否则存在环
    }

    // 方法二：dfs（判据：根据visited数组判断是否有环）-时间复杂度、空间复杂度-O(n+m)
    // 对于图中的任意一个节点，它在搜索的过程中有三种状态，即：
    // 「未搜索」：我们还没有搜索到这个节点；
    // 「搜索中」：我们搜索过这个节点，但还没有回溯到该节点，即该节点还没有入栈，还有相邻的节点没有搜索完成；
    // 「已完成」：我们搜索过并且回溯过这个节点，即该节点已经入栈，并且所有该节点的相邻节点都出现在栈的更底部的位置，满足拓扑排序的要求。
    // 将当前搜索的节点 u 标记为「搜索中」，遍历该节点的每一个相邻节点 v：
    // 如果 v 为「未搜索」，那么我们开始搜索 v，待搜索完成回溯到 u；
    // 如果 v 为「搜索中」，那么我们就找到了图中的一个环，因此是不存在拓扑排序的；
    // 如果 v 为「已完成」，那么说明 v 已经在栈中了，而 u 还不在栈中，
    // 因此 u 无论何时入栈都不会影响到 (u, v)之前的拓扑关系，以及不用进行任何操作。
    // 优化：
    // 由于我们只需要判断是否存在一种拓扑排序，而「栈的作用仅仅是存放最终的拓扑排序结果」，因此我们可以只记录每个节点的状态，而省去对应的栈。

    List<List<Integer>> edges;// 存放后继节点
    int[] visited;// 标记每个节点的状态：0=未搜索，1=搜索中，2=已完成
    boolean valid = true;

    public boolean canFinish2(int numCourses, int[][] prerequisites) {
        edges = new ArrayList<List<Integer>>();// 不定长顺序表
        for (int i = 0; i < numCourses; ++i)
            edges.add(new ArrayList<Integer>());

        visited = new int[numCourses];
        for (int[] info : prerequisites)
            edges.get(info[1]).add(info[0]);// 存放后继节点

        for (int i = 0; i < numCourses && valid; ++i)// 每一次遍历的是有向图的一个连通分支
            if (visited[i] == 0)// 未搜索
                dfs(i);

        return valid;
    }

    public void dfs(int u) {
        visited[u] = 1;// 标记为正在搜索的节点
        for (int v : edges.get(u)) {// 循环遍历该节点所有后继节点
            if (visited[v] == 0) {
                dfs(v);
                if (!valid)// 判断是否可提前退出递归
                    return;

            } else if (visited[v] == 1) {// 遇到正在搜索的节点了，即存在环
                valid = false;
                return;
            }
        }
        visited[u] = 2;// 标记为已完成
    }

    // 226. 翻转二叉树

    // 236. 二叉树的最近公共祖先

    // 297. 二叉树的序列化与反序列化

    // 337. 打家劫舍 III

    // 399. 除法求值
    // 给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件，
    // 其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。
    // 「每个 Ai 或 Bi 是一个表示单个变量的字符串」。
    // （用例 equation = ["ab", "cd"] ，这里的 ab 视为一个变量，不表示 a * b）
    // 另有一些以数组 queries 表示的问题，其中 queries[j] = [Cj, Dj] 表示第 j 个问题，
    // 请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。
    // 返回 所有问题的答案 。如果存在某个无法确定的答案，则用 -1.0 替代这个答案。
    // 如果问题中出现了给定的已知条件中没有出现的字符串，也需要用 -1.0 替代这个答案。
    // 注意：输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况，且不存在任何矛盾的结果。
    // 提示：
    // 1 <= equations.length <= 20
    // equations[i].length == 2
    // 1 <= Ai.length, Bi.length <= 5
    // values.length == equations.length
    // 0.0 < values[i] <= 20.0
    // 1 <= queries.length <= 20
    // queries[i].length == 2
    // 1 <= Cj.length, Dj.length <= 5
    // Ai, Bi, Cj, Dj 由小写英文字母与数字组成

    // 1.（带权）并查集-时间复杂度：O((N+Q)log A)
    // 构建并查集 O(NlogA) ，这里 N 为输入方程 equations 的长度，
    // 每一次执行合并操作的时间复杂度是 O(logA)，这里 A 是 equations 里不同字符的个数；
    // 查询并查集 O(QlogA)，这里 Q 为查询数组 queries 的长度，
    // 每一次查询时执行「路径压缩」的时间复杂度是 O(logA)。
    // 空间复杂度：O(A)：创建字符与 id 的对应关系 hashMap 长度为 A，
    // 并查集底层使用的两个数组 parent 和 weight 存储每个变量的连通分量信息，parent 和 weight 的长度均为 A。

    // 由于 变量之间的倍数关系具有传递性，处理有传递性关系的问题，可以使用「并查集」，
    // 我们需要在并查集的「合并」与「查询」操作中 维护这些变量之间的倍数关系。
    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        int equationsSize = equations.size();

        // equationsSize个等式，则最多2 * equationsSize个变量
        CalcEquationUnionFind unionFind = new CalcEquationUnionFind(2 * equationsSize);

        // 第 1 步：预处理，将变量的值与 id 进行映射，使得并查集的底层使用数组实现，方便编码
        Map<String, Integer> hashMap = new HashMap<>(2 * equationsSize);// 指定initialCapacity
        int id = 0;
        for (int i = 0; i < equationsSize; i++) {// 处理第i个式子
            List<String> equation = equations.get(i);
            String var1 = equation.get(0);// 被除数
            String var2 = equation.get(1);// 除数

            if (!hashMap.containsKey(var1)) {
                hashMap.put(var1, id);
                id++;
            }
            if (!hashMap.containsKey(var2)) {
                hashMap.put(var2, id);
                id++;
            }
            unionFind.union(hashMap.get(var1), hashMap.get(var2), values[i]);// 将两个变量对应的id传入，进行合并
        }

        // 第 2 步：做查询
        int queriesSize = queries.size();
        double[] res = new double[queriesSize];
        for (int i = 0; i < queriesSize; i++) {// 计算第i个式子
            String var1 = queries.get(i).get(0);
            String var2 = queries.get(i).get(1);

            Integer id1 = hashMap.get(var1);// 获取被除数对应的id
            Integer id2 = hashMap.get(var2);// 获取除数对应的id

            if (id1 == null || id2 == null)// 只要有一个不存在id（即，字符在equations中未出现）
                res[i] = -1.0d;
            else
                res[i] = unionFind.isConnected(id1, id2);

        }
        return res;
    }

    private class CalcEquationUnionFind {// 内部类

        private int[] parent;// 各节点的父节点

        private double[] weight;// 指向的父节点的权值（除法的值）

        public CalcEquationUnionFind(int n) {// 初始化，n个变量
            this.parent = new int[n];
            this.weight = new double[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;// 初始化时，每个节点的父节点都默认是自己
                weight[i] = 1.0d;// 除法（自己除以自己）值为1
            }
        }

        public void union(int x, int y, double value) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY)// 两节点父节点为同一个，则无需合并
                return;

            parent[rootX] = rootY;// 被除数祖先的父节点为除数的祖先

            weight[rootX] = weight[y] * value / weight[x];// 节点x / 节点x的父节点 = weight[x]
        }

        // 路径压缩，寻找父节点的同时，修改节点指向祖先节点，且修改weight值
        public int find(int x) {
            if (x != parent[x]) {
                int origin = parent[x];
                parent[x] = find(parent[x]);// 修改父节点指向，指向祖先节点
                weight[x] *= weight[origin];// 权值乘以父节点到祖先节点的权值
            }
            return parent[x];
        }

        public double isConnected(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY)// x, y父节点相同
                return weight[x] / weight[y]; // (x / x, y共同的父节点) / (y / x, y共同的父节点) = x / y
            else
                return -1.0d;// 不在一个集合里，则无法通过已有等式计算得到

        }
    }

    // 方法一：自己写的并查集
    // 合并精髓：second父节点指向first父节点
    public double[] calcEquation1(List<List<String>> equations, double[] values, List<List<String>> queries) {
        Map<String, String> fathers = new HashMap<>();// 存储父节点或祖先节点，每次查询父节点后，修改为祖先节点（路径压缩）
        Map<String, Double> vals = new HashMap<>();// 存储父节点或祖先节点 / 当前节点 的比值，每次查询父节点后，修改为祖先节点（路径压缩）
        for (int i = 0; i < values.length; i++) {
            List<String> equation = equations.get(i);// first / second
            double value = values[i];
            String first = equation.get(0);
            String second = equation.get(1);

            if (!fathers.containsKey(first)) {// 未遇到过 first
                fathers.put(first, first);
                vals.put(first, 1.0);
            }
            if (!fathers.containsKey(second)) {// 未遇到过 second
                fathers.put(second, second);
                vals.put(second, 1.0);
            }

            String firstFather = getFather(fathers, vals, first);
            String secondFather = getFather(fathers, vals, second);
            if (firstFather == secondFather) // 祖先节点相同，无需合并
                continue;

            double firstVal = vals.get(first);
            double secondVal = vals.get(second);
            fathers.put(secondFather, firstFather);// second 的祖先节点指向 first 的祖先节点
            vals.put(secondFather, firstVal * value / secondVal);
        }

        double res[] = new double[queries.size()];
        for (int i = 0; i < queries.size(); i++) {
            List<String> query = queries.get(i);
            String first = query.get(0);
            String second = query.get(1);
            if (!fathers.containsKey(first) || !fathers.containsKey(second)) {// 至少有一个未出现过
                res[i] = -1.0;
                continue;
            }
            String firstFather = getFather(fathers, vals, first);
            String secondFather = getFather(fathers, vals, second);
            if (firstFather != secondFather) {// 祖先节点不相同
                res[i] = -1.0;
                continue;
            }
            double firstVal = vals.get(first);
            double secondVal = vals.get(second);
            res[i] = secondVal / firstVal;
        }
        return res;
    }

    // 一边查找父节点，一边修改指向和值
    private String getFather(Map<String, String> fathers, Map<String, Double> vals, String str) {
        String father = fathers.get(str);
        if (father == str)
            return father;
        else {
            String ancestor = getFather(fathers, vals, father);
            fathers.put(str, ancestor);
            vals.put(str, vals.get(father) * vals.get(str));
            return ancestor;
        }
    }

    // 2.bfs-时间复杂度：O(ML+Q(L+M))
    // 其中 M 为边的数量，Q 为询问的数量，L 为字符串的平均长度。
    // 构建图时，需要处理 M 条边，每条边都涉及到 O(L) 的字符串比较；
    // 处理查询时，每次查询首先要进行一次 O(L) 的比较，然后至多遍历 O(M) 条边。
    // 空间复杂度：O(NL+M)，其中 N 为点的数量，M 为边的数量，L 为字符串的平均长度。
    // 为了将每个字符串映射到整数，需要开辟空间为 O(NL) 的哈希表；随后，需要花费 O(M) 的空间存储每条边的权重；
    // 处理查询时，还需要 O(N) 的空间维护访问队列。
    // 最终，总的复杂度为 O(NL+M+N) =O(NL+M)
    public double[] calcEquation2(List<List<String>> equations, double[] values, List<List<String>> queries) {
        // 将变量的值与 id 进行映射，使得并查集的底层使用数组实现，方便编码
        Map<String, Integer> variables = new HashMap<String, Integer>();
        int nvars = 0;
        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (!variables.containsKey(equations.get(i).get(0)))
                variables.put(equations.get(i).get(0), nvars++);

            if (!variables.containsKey(equations.get(i).get(1)))
                variables.put(equations.get(i).get(1), nvars++);
            // 相比于并查集，并查集会在映射的同时，对节点进行合并，后续查询时会更快，时间复杂度O(n)
            // bfs的查询，每一个式子都要一次bfs
        }

        // 对于每个点，存储其直接连接到的所有点及对应的权值
        @SuppressWarnings("unchecked")
        List<Pair>[] edges = new ArrayList[nvars];// 相比无权图，需要用一个Pair存储权值和邻接边的信息
        for (int i = 0; i < nvars; i++)
            edges[i] = new ArrayList<Pair>();

        for (int i = 0; i < n; i++) {// 每一个式子：a / b
            int va = variables.get(equations.get(i).get(0)), vb = variables.get(equations.get(i).get(1));
            edges[va].add(new Pair(vb, values[i]));// a / b
            edges[vb].add(new Pair(va, 1.0 / values[i]));// b / a
        }

        int queriesCount = queries.size();
        double[] ret = new double[queriesCount];
        for (int i = 0; i < queriesCount; i++) {// 查询求解，每一个式子都要一次bfs
            List<String> query = queries.get(i);
            double result = -1.0;// 没找到解的默认返回
            if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {// 除数和被除数同时存在映射
                int ia = variables.get(query.get(0)), ib = variables.get(query.get(1));
                if (ia == ib)
                    result = 1.0;
                else {// bfs
                    Queue<Integer> points = new LinkedList<Integer>();
                    points.offer(ia);// 入队
                    double[] ratios = new double[nvars];// 每次bfs都要重复计算，遍历到该点x时，赋值：a / x = a / 中间点 * 中间点 / x
                    // Assigns the specified double value (-1.0) to each element of the specified
                    // array
                    Arrays.fill(ratios, -1.0);
                    ratios[ia] = 1.0;// a / a = 1.0

                    while (!points.isEmpty() && ratios[ib] < 0) {// ratios[ib]不为0则表示已找到最终答案，可提前退出循环
                        int x = points.poll();
                        for (Pair pair : edges[x]) {
                            int y = pair.index;
                            double val = pair.value;
                            if (ratios[y] < 0) {// 此时y未遍历到，已遍历过则不入队
                                ratios[y] = ratios[x] * val;
                                points.offer(y);// 入队
                            }
                        }
                    }
                    result = ratios[ib];// 如果bfs未遍历到ib，则默认值-1.0，若遍历到，则为最终答案
                }
            }
            ret[i] = result;
        }
        return ret;
    }

    private class Pair {// 内部类
        int index;// 邻接边的索引
        double value;// 权值

        Pair(int index, double value) {
            this.index = index;
            this.value = value;
        }
    }

    // 3. 自己写的lowb dfs-时间复杂度：O(ML+Q(L+M))
    // 空间复杂度：O(NL+M)
    boolean findQuery;// 是否已经找到最终答案
    double[] ratios;// 被除数 / 所有节点
    int ib;// 每个求解式子的除数

    public double[] calcEquation3(List<List<String>> equations, double[] values, List<List<String>> queries) {
        // 将变量的值与 id 进行映射，使得并查集的底层使用数组实现，方便编码
        Map<String, Integer> variables = new HashMap<String, Integer>();
        int nvars = 0;
        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (!variables.containsKey(equations.get(i).get(0)))
                variables.put(equations.get(i).get(0), nvars++);

            if (!variables.containsKey(equations.get(i).get(1)))
                variables.put(equations.get(i).get(1), nvars++);
            // 相比于并查集，并查集会在映射的同时，对节点进行合并，后续查询时会更快，时间复杂度O(n)
            // bfs的查询，每一个式子都要一次bfs
        }
        // 对于每个点，存储其直接连接到的所有点及对应的权值
        @SuppressWarnings("unchecked")
        List<Pair>[] edges = new ArrayList[nvars];// 相比无权图，需要用一个Pair存储权值和邻接边的信息
        for (int i = 0; i < nvars; i++)
            edges[i] = new ArrayList<Pair>();
        for (int i = 0; i < n; i++) {// 每一个式子：a / b
            int va = variables.get(equations.get(i).get(0)), vb = variables.get(equations.get(i).get(1));
            edges[va].add(new Pair(vb, values[i]));// a / b
            edges[vb].add(new Pair(va, 1.0 / values[i]));// b / a
        }

        int queriesCount = queries.size();
        double[] ret = new double[queriesCount];
        for (int i = 0; i < queriesCount; i++) {// 查询求解，每一个式子都要一次dfs
            List<String> query = queries.get(i);
            double result = -1.0;// 没找到解的默认返回
            if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {// 除数和被除数同时存在映射
                int ia = variables.get(query.get(0));// 被除数a
                ib = variables.get(query.get(1)); // 除数b
                if (ia == ib)
                    result = 1.0;
                else {// dfs
                    findQuery = false;
                    ratios = new double[nvars];// 每次dfs都要重复计算，遍历到该点x时，赋值：a / x = a / 中间点 * 中间点 / x
                    // Assigns the specified double value (-1.0) to each element of the specified
                    // array
                    Arrays.fill(ratios, -1.0);
                    ratios[ia] = 1.0;// a / a = 1.0
                    dfs(edges, ia);
                    result = ratios[ib];// 如果bfs未遍历到ib，则默认值-1.0，若遍历到，则为最终答案
                }
            }
            ret[i] = result;
        }
        return ret;
    }

    void dfs(List<Pair>[] edges, int x) {
        if (findQuery)// 提前结束循环
            return;

        for (Pair p : edges[x]) {
            int y = p.index;// 节点x的邻接节点
            if (y == ib) {
                findQuery = true;
                ratios[ib] = ratios[x] * p.value;// a/x * x/b
                return;
            }
            if (ratios[y] < 0) {// 未遍历到
                ratios[y] = ratios[x] * p.value;// a/x * x/y
                dfs(edges, y);
            }
            if (findQuery)// 提前结束循环
                return;
        }

    }
    // 4.Floyd算法（这个最简单了，上面几个写麻了）-时间复杂度：O(ML+N3+QL)。
    // 构建图需要 O(ML) 的时间；Floyd 算法需要O(N3) 的时间；处理查询时，单次查询只需要 O(L) 的字符串比较以及常数时间的额外操作。
    // 空间复杂度：O(NL+N2)。

    // 对于查询数量很多的情形，如果为每次查询都独立搜索一次，则效率会变低。为此，我们不妨对图先做一定的预处理，随后就可以在较短的时间内回答每个查询。
    // 在本题中，我们可以使用 Floyd 算法，预先计算出任意两点之间的距离。
    public double[] calcEquation4(List<List<String>> equations, double[] values, List<List<String>> queries) {
        int nvars = 0;
        Map<String, Integer> variables = new HashMap<String, Integer>();// String 到 int 的映射

        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (!variables.containsKey(equations.get(i).get(0)))
                variables.put(equations.get(i).get(0), nvars++);

            if (!variables.containsKey(equations.get(i).get(1)))
                variables.put(equations.get(i).get(1), nvars++);

        }

        double[][] graph = new double[nvars][nvars];// Floyd算法中使用邻接矩阵来表示图的边
        for (int i = 0; i < nvars; i++)
            Arrays.fill(graph[i], -1.0);// 初值赋为-1.0表示两点不邻接

        for (int i = 0; i < n; i++) {
            int va = variables.get(equations.get(i).get(0)), vb = variables.get(equations.get(i).get(1));
            graph[va][vb] = values[i];// 坐标(a, b)的权值表示 a / b
            graph[vb][va] = 1.0 / values[i];// (b, a) b / a
            // 这里没有赋值[va][va]=1.0 [vb][vb]=1.0 是因为floyd算法会计算这种情况
        }

        // floyd算法模板 k在最外层！
        for (int k = 0; k < nvars; k++)// 存在一定量的重复计算
            for (int i = 0; i < nvars; i++)
                for (int j = 0; j < nvars; j++)
                    if (graph[i][k] > 0 && graph[k][j] > 0) // 节点 i 与 k 邻接且 k 与 j 邻接
                        graph[i][j] = graph[i][k] * graph[k][j];// 则可通过中间点 k 计算得到 i / j

        int queriesCount = queries.size();
        double[] ret = new double[queriesCount];
        for (int i = 0; i < queriesCount; i++) {
            List<String> query = queries.get(i);
            double result = -1.0;
            if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {
                int ia = variables.get(query.get(0)), ib = variables.get(query.get(1));
                if (graph[ia][ib] > 0)
                    result = graph[ia][ib];

            }
            ret[i] = result;
        }
        return ret;
    }

    // 方法四：自己写的floyd算法
    public double[] calcEquation44(List<List<String>> equations, double[] values, List<List<String>> queries) {
        int varNum = 0;
        Map<String, Integer> map = new HashMap<>();
        for (List<String> equation : equations) {
            String first = equation.get(0);
            String second = equation.get(1);
            if (!map.containsKey(first))
                map.put(first, varNum++);

            if (!map.containsKey(second))
                map.put(second, varNum++);

        }

        double graph[][] = new double[varNum][varNum];
        for (int i = 0; i < varNum; i++)
            Arrays.fill(graph[i], -1.0);

        for (int i = 0; i < equations.size(); i++) {
            List<String> equation = equations.get(i);
            String first = equation.get(0);
            String second = equation.get(1);
            double value = values[i];
            int firstIndex = map.get(first);
            int secondIndex = map.get(second);

            graph[firstIndex][secondIndex] = value;
            graph[secondIndex][firstIndex] = 1.0 / value;
        }

        for (int k = 0; k < varNum; k++) {
            for (int i = 0; i < varNum; i++) {
                for (int j = 0; j < varNum; j++) {
                    if (graph[i][j] < 0) {
                        if (graph[i][k] > 0 && graph[k][j] > 0) {
                            graph[i][j] = graph[i][k] * graph[k][j];
                        }
                    }
                }
            }
        }
        double res[] = new double[queries.size()];
        for (int i = 0; i < queries.size(); i++) {
            List<String> query = queries.get(i);
            String first = query.get(0);
            String second = query.get(1);
            if (!map.containsKey(first) || !map.containsKey(second)) {
                res[i] = -1.0;
                continue;
            }
            int firstIndex = map.get(first);
            int secondIndex = map.get(second);
            res[i] = graph[firstIndex][secondIndex];

        }
        return res;
    }

    // 437. 路径总和 III
    // 538. 把二叉搜索树转换为累加树
    // 543. 二叉树的直径
    // 617. 合并二叉树

}

// 广度优先遍历
class BFSSol {
    // 101.对称二叉树
    // 102.二叉树的层序遍历
    // 104.二叉树的最大深度
    // 200.岛屿数量
    // 207.课程表
    // 226.翻转二叉树

    // 279.完全平方数
    // 给定正整数 n，找到若干个完全平方数（比如 1, 4, 9, 16, ...）使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
    // 给你一个整数 n ，返回和为 n 的完全平方数的 最少数量 。
    // 完全平方数 是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，1、4、9 和 16 都是完全平方数，而 3 和 11 不是。
    // 提示：
    // 1 <= n <= 104

    // 方法一：动态规划-时间复杂度：O(n√n)，空间复杂度：O(n)
    int maxInput = 1;
    int[] f = new int[10000];
    {
        f[1] = 1;
    }

    public int numSquares(int n) {
        if (n <= maxInput)
            return f[n];

        for (int i = maxInput; i <= n; i++) {// 改进版，不用每次重复从1开始算起
            int minn = Integer.MAX_VALUE;
            for (int j = 1; j * j <= i; j++)// 核心：只用枚举√n个完全平方数
                minn = Math.min(minn, f[i - j * j]);
            f[i] = minn + 1;
        }
        maxInput = n;
        return f[n];
    }

    // 方法二：数学-时间复杂度：O(√n)，空间复杂度：O(1)
    // 一个数学定理可以帮助解决本题: 「四平方和定理」。
    // 四平方和定理证明了任意一个正整数都可以被表示为至多 四个正整数的平方和。这给出了本题的答案的上界。
    // 同时四平方和定理包含了一个更强的结论:
    // 当且仅当n ≠ 4k x (8m+7)时，n可以被表示为至多三个正整数的平方和。
    // 因此，当n=4k x (8m+7)时，n只能被表示为四个正整数的平方和。此时我们可以直接返回4。
    // 当n≠4k x (8m+7)时，我们需要判断到底多少个完全平方数能够表示n,我们知道答案只会是1,2,3中的一个:
    // 1.答案为1时，则必有n为完全平方数，这很好判断;
    // 2.答案为2时，则有n=a2+b2,我们只需要枚举所有的a(1≤a≤√n), 判断n - a2是否为完全平方
    // 数即可;
    // 3.答案为3时，我们很难在一个优秀的时间复杂度内解决它，但我们只需要检查答案为1或2的两种情
    // 况，即可利用排除法确定答案。
    public int numSquares2(int n) {
        if (isPerfectSquare(n)) // 判断是否为完全平方数
            return 1;

        if (checkAnswer4(n)) // 判断是否能表示为 4^k*(8m+7)
            return 4;

        for (int i = 1; i * i <= n; i++) {
            int j = n - i * i;
            if (isPerfectSquare(j))
                return 2;

        }
        return 3;
    }

    // 判断是否为完全平方数
    public boolean isPerfectSquare(int x) {
        int y = (int) Math.sqrt(x);
        return y * y == x;
    }

    // 判断是否能表示为 4^k*(8m+7)
    public boolean checkAnswer4(int x) {
        while (x % 4 == 0)
            x /= 4;

        return x % 8 == 7;
    }

    // 297.二叉树的序列化与反序列化

    // 301.删除无效的括号
    // 给你一个由若干括号和字母组成的字符串 s ，删除最小数量的无效括号，使得输入的字符串有效。
    // 返回所有可能的结果。答案可以按 任意顺序 返回。

    // 方法一：回溯（dfs）-复杂度分析比较复杂，略
    // （本方法剪枝：遍历到右括号时，如果当前右括号等于左括号，剪枝）
    // （增加剪枝，可以考虑leftleft，rightleft和leftremove和rightremove比较？）
    // 题目让我们删除括号使得剩下的括号匹配，要求我们删除最少的括号数，并且要求得到所有的结果。
    // 这样的问题提示我们须要使用 不断尝试 的遍历算法，即 回溯算法；
    // 一次遍历计算出多余的「左括号」和「右括号」：（可能同时需要删除左右括号）
    // 1.当遍历到「右括号」的时候，
    // 1.1.如果此时「左括号」的数量不为 0，因为 「右括号」可以与之前遍历到的「左括号」匹配，此时「左括号」出现的次数 −1；
    // 1.2.如果此时「左括号」的数量为 0，「右括号」数量加 1；
    // 2.当遍历到「左括号」的时候，「左括号」数量加 1。
    // 通过这样的计数规则，最后「左括号」和「右括号」的数量就是各自最少应该删除的数量。

    private int len;
    private char[] charArray;
    private Set<String> validExpressions = new HashSet<>();// 去重使用「哈希表」

    public List<String> removeInvalidParentheses(String s) {
        this.len = s.length();
        this.charArray = s.toCharArray();

        // 第 1 步：遍历一次，计算多余的左右括号
        int leftRemove = 0;
        int rightRemove = 0;
        for (int i = 0; i < len; i++) {
            if (charArray[i] == '(')
                leftRemove++;
            else if (charArray[i] == ')') {
                // 遇到右括号的时候，须要根据已经存在的左括号数量决定
                if (leftRemove == 0)
                    rightRemove++;

                if (leftRemove > 0)
                    // 关键：一个右括号出现可以抵销之前遇到的左括号
                    leftRemove--;
            }
        }

        // 第 2 步：回溯算法，尝试每一种可能的删除操作
        StringBuilder path = new StringBuilder();
        dfs(0, 0, 0, leftRemove, rightRemove, path);
        return new ArrayList<>(this.validExpressions);
    }

    /**
     * @param index       当前遍历到的下标
     * @param leftCount   已经遍历到的左括号的个数
     * @param rightCount  已经遍历到的右括号的个数
     * @param leftRemove  最少应该删除的左括号的个数
     * @param rightRemove 最少应该删除的右括号的个数
     * @param path        一个可能的结果
     */
    // leftCount 和 rightCount 代替了栈，来判断括号格式
    private void dfs(int index, int leftCount, int rightCount, int leftRemove, int rightRemove, StringBuilder path) {
        if (index == len) { // 遍历完，剪枝
            if (leftRemove == 0 && rightRemove == 0) // 一种正确结果
                validExpressions.add(path.toString());

            return;
        }

        char character = charArray[index];
        // 字符：保留；左括号：删除或保留，右括号：删除或保留

        // 可能的操作 1：删除当前遍历到的字符
        if (character == '(' && leftRemove > 0)
            // 由于 leftRemove > 0，并且当前遇到的是「左括号」，因此可以尝试删除当前遇到的左括号
            dfs(index + 1, leftCount, rightCount, leftRemove - 1, rightRemove, path);

        if (character == ')' && rightRemove > 0)
            // 由于 rightRemove > 0，并且当前遇到的是「右括号」，因此可以尝试删除当前遇到的右括号
            dfs(index + 1, leftCount, rightCount, leftRemove, rightRemove - 1, path);

        // 可能的操作 2：保留当前遍历到的字符
        path.append(character);
        if (character != '(' && character != ')')
            // 如果「不是括号」，继续深度优先遍历
            dfs(index + 1, leftCount, rightCount, leftRemove, rightRemove, path);
        else if (character == '(')
            // 「左括号」无条件保留
            dfs(index + 1, leftCount + 1, rightCount, leftRemove, rightRemove, path);
        else if (rightCount < leftCount)
            // 一定要当前右括号少于左括号，才考虑保留「右括号」
            dfs(index + 1, leftCount, rightCount + 1, leftRemove, rightRemove, path);

        path.deleteCharAt(path.length() - 1);
    }

    // 方法一：（自己写的）回溯（dfs）-复杂度分析比较复杂，略
    // 递归时需要的参数其实没有那么多
    Set<String> set = new HashSet<>();
    Deque<Character> stk = new LinkedList<>();
    StringBuilder ans = new StringBuilder();
    String s;

    public List<String> removeInvalidParentheses11(String s) {
        this.s = s;
        int leftDelete = 0;
        int rightDelete = 0;

        // 确定需要删除的左右括号
        Deque<Character> stk = new LinkedList<>();
        for (char c : s.toCharArray()) {
            if (c == '(')
                stk.push(c);
            else if (c == ')') {
                if (!stk.isEmpty() && stk.peek() == '(')
                    stk.poll();
                else
                    rightDelete++;
            }
        }
        leftDelete = stk.size();
        dfs(0, leftDelete, rightDelete);
        return new ArrayList<>(set);
    }

    private void dfs(int index, int leftDelete, int rightDelete) {
        if (index == s.length()) {
            if (stk.isEmpty())
                set.add(new String(ans));
            return;
        }

        char curr = s.charAt(index);
        if (curr == ')') {
            if (rightDelete > 0)
                dfs(index + 1, leftDelete, rightDelete - 1);

            if (!stk.isEmpty() && stk.peek() == '(') {
                // 对于不同栈帧共用的变量，操作后一定要还原！
                stk.poll();
                ans.append(curr);
                dfs(index + 1, leftDelete, rightDelete);
                stk.push('(');
                ans.deleteCharAt(ans.length() - 1);
            }
        } else if (curr == '(') {
            if (leftDelete > 0)
                dfs(index + 1, leftDelete - 1, rightDelete);

            stk.push(curr);
            ans.append(curr);
            dfs(index + 1, leftDelete, rightDelete);
            stk.poll();
            ans.deleteCharAt(ans.length() - 1);
        } else {// 其他字符
            ans.append(curr);
            dfs(index + 1, leftDelete, rightDelete);
            ans.deleteCharAt(ans.length() - 1);
        }

    }

    // 方法二：bfs（有点暴力，无剪枝）-复杂度分析比较复杂，略
    // （优化：增加剪枝）
    public List<String> removeInvalidParentheses2(String s) {
        List<String> res = new ArrayList<>();
        if (s == null)
            return res;

        Set<String> visited = new HashSet<>();// 防止重复遍历的哈希表 visited
        visited.add(s);
        Queue<String> queue = new LinkedList<>();// 广度优先遍历需要的队列
        queue.offer(s);

        // 找到目标值以后退出循环
        boolean found = false;
        while (!queue.isEmpty()) {
            int size = queue.size();// 该层循环开始时，所有情况的个数。（循环中queue的size会变化）
            // 最优解一定在同一层（删除最小数量的无效括号）
            for (int i = 0; i < size; i++) {// 遍历队中所有情况，每一层的各情况括号数相等
                String front = queue.poll();// 出队
                if (isValid(front)) {// 检查出队字符串是否符合要求
                    res.add(front);
                    found = true;
                }

                int currentWordLen = front.length();
                char[] charArray = front.toCharArray();
                for (int j = 0; j < currentWordLen; j++) {
                    if (front.charAt(j) != '(' && front.charAt(j) != ')') // 跳过非括号，即字母
                        continue;

                    // 注意 new String() 方法的 API，第 1 个参数是字符数组，第 2 个参数是字符数组的起始下标，第 3 个参数是截取的字符的长度
                    String next = new String(charArray, 0, j) + new String(charArray, j + 1, currentWordLen - j - 1);// 删除第j个位置字符
                    if (!visited.contains(next)) {
                        queue.offer(next);// 入队
                        visited.add(next);
                    }
                }
            }

            // 这一层找到以后，退出外层循环，返回结果
            if (found)
                break;

        }
        return res;
    }

    public boolean isValid(String s) {
        char[] charArray = s.toCharArray();
        int count = 0;
        for (char c : charArray) {
            if (c == '(')
                count++;
            else if (c == ')')
                count--;

            if (count < 0)
                return false;

        }
        return count == 0;
    }

    // // 322.零钱兑换
    // 给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。
    // 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。
    // 你可以认为每种硬币的数量是无限的。
    // 提示：
    // 1 <= coins.length <= 12
    // 1 <= coins[i] <= 231 - 1 没有面值为0的币
    // 0 <= amount <= 104

    // 方法一：动态规划-时间复杂度：O(Sn)，其中 S 是金额，n 是面额数。
    // 我们一共需要计算 O(S) 个状态，S 为题目所给的总金额。对于每个状态，每次需要枚举 n 个面额来转移状态。
    // 空间复杂度：O(S)。
    // dp的下标表示要凑出面额
    // 先确定末尾（要凑出的面额）（防止数据溢出）
    public int coinChange(int[] coins, int amount) {

        int max = amount + 1;// 最终答案的最大值为amount（全部由面额1组成）
        int[] dp = new int[amount + 1];// dp的下标表示要凑出面额
        Arrays.fill(dp, max);// 初始化为amount + 1 方便后面比较取小，且只要最终答案的值大于max，一定无法凑出该面额
        dp[0] = 0;// 方便后续凑

        for (int i = 1; i <= amount; i++) // 期望凑到面额i，amount=0则直接跳出循环返回-1
            for (int coin : coins) // 尝试使用各种面额凑到面额i，面额i =（总额i-该面额）+ 该面额
                if (coin <= i)
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);

        return dp[amount] >= max ? -1 : dp[amount];
    }

    // 方法二：动态规划-时间复杂度：O(Sn)，空间复杂度：O(S)。
    // 先确定起始（从该面额开始凑）
    public int coinChange2(int[] coins, int amount) {

        int max = amount + 1;// 最终答案的最大值为amount（全部由面额1组成）
        int[] dp = new int[amount + 1];// dp的下标表示要凑出面额
        Arrays.fill(dp, max);// 初始化为amount + 1 方便后面比较取小，且只要最终答案的值大于max，一定无法凑出该面额
        dp[0] = 0;// 方便后续凑

        for (int i = 0; i <= amount; i++) // 从总额i开始凑，amount=0则直接跳出循环返回-1
            for (int coin : coins) // （总额i-该面额）+ 各面额
                if (i + (long) coin <= amount)// 样例有整型最大值的面额，醉了
                    dp[i + coin] = Math.min(dp[i + coin], dp[i] + 1);

        return dp[amount] >= max ? -1 : dp[amount];
    }

    // 方法三：dfs 记忆化搜索-时间复杂度：O(Sn)，其中 S 是金额，n 是面额数。空间复杂度：O(S)
    // 为了避免重复的计算，我们将每个子问题的答案存在一个数组中进行记忆化，如果下次还要计算这个问题的值直接从数组中取出返回即可，
    // 这样能保证每个子问题最多只被计算一次。
    public int coinChange3(int[] coins, int amount) {
        if (amount < 1)
            return 0;

        return coinChange(coins, amount, new int[amount]);
    }

    private int coinChange(int[] coins, int rem, int[] count) {
        if (rem < 0)
            return -1;

        if (rem == 0)
            return 0;

        if (count[rem - 1] != 0)// 通过数组避免一些重复计算，对于每个不同的问题（不同面额的币），count都不相同
            return count[rem - 1];

        int min = Integer.MAX_VALUE;
        for (int coin : coins) {
            int res = coinChange(coins, rem - coin, count); // 选择一种币，分解成几个子问题
            if (res >= 0 && res < min) // res=-1则表示无法凑出
                min = 1 + res;

        }
        count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min;// 更新数组
        return count[rem - 1];
    }

    // 399.除法求值
    // 617.合并二叉树

}

// 并查集
class UnionFindSetsSol {

    // 128. 最长连续序列
    // 给定一个未排序的整数数组 nums ，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。
    // 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。（人为的排序要nlogn）

    // 方法一：哈希表-时空复杂度：O(n)
    // 不含前驱，开始找它的后继
    public int longestConsecutive(int[] nums) {
        Set<Integer> num_set = new HashSet<>();// Set去重
        for (int num : nums)
            num_set.add(num);

        int longestStreak = 0;

        for (int num : num_set)// 重点：遍历集合内元素（去重），而不是数组内
            if (!num_set.contains(num - 1)) {// 不含前驱，开始找它的后继
                int currentNum = num;
                int currentStreak = 1;

                while (num_set.contains(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }

                longestStreak = Math.max(longestStreak, currentStreak);
            }

        return longestStreak;
    }

    // 200. 岛屿数量

    // 399. 除法求值
}

// 图
class GraphSol {
    // 207.课程表
    // 399.除法求值
}

// 设计
class DesignSol {
    // 146. LRU缓存机制
    // 运用你所掌握的数据结构，设计和实现一个 LRU (最近最少使用) 缓存机制。
    // 实现 LRUCache 类：
    // LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
    // int get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。
    // void put(int key, int value) 如果关键字已经存在，则变更其数据值；如果关键字不存在，则插入该组「关键字-值」。
    // 当缓存容量达到上限时，它应该在写入新数据之前删除最久未使用的数据值，从而为新的数据值留出空间。
    // 进阶：你是否可以在 O(1) 时间复杂度内完成这两种操作？

    // 方法一：哈希表 + 双向链表-时间复杂度：O(1)，空间复杂度：O(capacity)
    // 在面试中这种设计题，面试官一般会期望读者能够自己实现一个简单的双向链表，而不是使用语言自带的、封装好的数据结构。
    // 在双向链表的实现中，使用一个伪头部（dummy head）和伪尾部（dummy tail）标记界限，
    // 这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
    class LRUCache {
        class DLinkedNode {// 双向链表节点
            int key;
            int value;
            DLinkedNode prev;
            DLinkedNode next;

            public DLinkedNode() {
            }

            public DLinkedNode(int _key, int _value) {
                key = _key;
                value = _value;
            }
        }

        private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
        private int size;// 当前缓存内个数
        private int capacity;// 最大容量，初始化时定义
        private DLinkedNode dummyHead, dummyTail;

        public LRUCache(int capacity) {
            this.size = 0;
            this.capacity = capacity;
            // 使用伪头部和伪尾部节点
            dummyHead = new DLinkedNode();
            dummyTail = new DLinkedNode();
            dummyHead.next = dummyTail;
            dummyTail.prev = dummyHead;
        }

        public int get(int key) {
            DLinkedNode node = cache.get(key);
            if (node == null)
                return -1;

            // 如果 key 存在，先通过哈希表定位，再移到头部
            moveToHead(node);
            return node.value;
        }

        public void put(int key, int value) {
            DLinkedNode node = cache.get(key);
            if (node == null) {// 如果 key 不存在
                DLinkedNode newNode = new DLinkedNode(key, value);// 创建一个新的节点
                cache.put(key, newNode); // 添加进哈希表
                addToHead(newNode); // 添加至双向链表的头部
                ++size;
                if (size > capacity) {
                    // 如果超出容量，删除双向链表的尾部节点
                    DLinkedNode tail = removeTail();
                    // 删除哈希表中对应的项
                    cache.remove(tail.key);
                    --size;
                }
            } else {
                // 如果 key 存在，先通过哈希表定位，再修改 value，并移到头部
                node.value = value;
                moveToHead(node);
            }
        }

        private void addToHead(DLinkedNode node) {
            DLinkedNode prevHead = dummyHead.next;
            node.prev = dummyHead;
            node.next = prevHead;
            prevHead.prev = node;
            dummyHead.next = node;
        }

        private void removeNode(DLinkedNode node) {
            DLinkedNode prevNode = node.prev;
            DLinkedNode nextNode = node.next;
            prevNode.next = nextNode;
            nextNode.prev = prevNode;
        }

        private void moveToHead(DLinkedNode node) {
            removeNode(node);
            addToHead(node);
        }

        private DLinkedNode removeTail() {
            DLinkedNode res = dummyTail.prev;
            removeNode(res);
            return res;
        }
    }

    // 方法二：自己写的lowb，用java自带的LinkedHashMap-时间复杂度：O(1)，空间复杂度：O(capacity)
    class LRUCache2 {
        Map<Integer, Integer> cache;
        int capacity;
        int size;

        public LRUCache2(int capacity) {
            this.capacity = capacity;
            cache = new LinkedHashMap<>(capacity);
        }

        public int get(int key) {
            if (cache.containsKey(key)) {
                Integer value = cache.get(key);
                cache.remove(key);
                cache.put(key, value);
                return value;
            } else
                return -1;
        }

        public void put(int key, int value) {
            if (cache.containsKey(key)) {
                cache.remove(key);
                cache.put(key, value);
            } else {
                ++size;
                if (size > capacity) {
                    Set<Integer> keySet = cache.keySet();
                    Iterator<Integer> keyIterator = keySet.iterator();
                    cache.remove(keyIterator.next()); // 一定不用判断hasNext()
                    --size;
                }
                cache.put(key, value);
            }
        }
    }

    // 155.最小栈

    // 208.实现Trie (前缀树)
    // Trie（发音类似 "try"）或者说 前缀树 是一种树形数据结构，用于高效地存储和检索字符串数据集中的键。
    // 这一数据结构有相当多的应用情景，例如自动补完和拼写检查。
    // 请你实现 Trie 类：
    // Trie() 初始化前缀树对象。
    // void insert(String word)
    // 向前缀树中插入字符串 word 。
    // boolean search(String word)
    // 如果字符串 word 在前缀树中，返回 true（即，在检索之前已经插入）；否则，返回 false。
    // boolean startsWith(String prefix)
    // 如果之前已经插入的字符串 word 的前缀之一为 prefix ，返回 true ；否则，返回 false。
    // 提示：
    // 1 <= word.length, prefix.length <= 2000
    // word 和 prefix 仅由小写英文字母组成
    // insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次

    // 方法一：字典树-时间复杂度：初始化为O(1)，其余操作为 O(|S|)，其中 |S| 是每次插入或查询的字符串的长度。
    // 空间复杂度：O(|T|⋅Σ)，其中 |T| 为所有插入字符串的长度之和，Σ 为字符集的大小，本题 Σ=26。
    // Trie，又称前缀树或字典树，是一棵有根树，其每个节点包含以下字段：
    // 指向子节点的指针数组 children。对于本题而言，数组长度为 26，即小写英文字母的数量。
    // 此时 children[0] 对应小写字母 a，children[1] 对应小写字母 b，…，children[25] 对应小写字母 z。
    // 布尔字段 isEnd，表示该节点是否为字符串的结尾。
    // （本题可看成一棵26叉树，以边表示字母）
    class Trie {
        TrieNode headNode;

        Trie() {
            headNode = new TrieNode();
        }

        private class TrieNode {
            TrieNode[] children;
            boolean isEnd; // 默认false

            TrieNode() {
                children = new TrieNode[26];
            }
        }

        public void insert(String word) {
            TrieNode node = headNode;// node指向头节点
            for (int i = 0; i < word.length(); i++) {// 逐个遍历每个字符
                char ch = word.charAt(i);
                int index = ch - 'a'; // 从a开始索引值为0
                if (node.children[index] == null) // 若无则创建节点
                    node.children[index] = new TrieNode();

                node = node.children[index];// 指向当前字符
            }
            node.isEnd = true;// 表示字符串结尾
        }

        public boolean search(String word) {
            TrieNode node = searchPrefix(word);
            return node != null && node.isEnd;// 存在该字符串，且为字符串结尾
        }

        public boolean startsWith(String prefix) {
            return searchPrefix(prefix) != null;// 无需为字符串结尾
        }

        // 查询前缀，返回末尾字符节点
        private TrieNode searchPrefix(String prefix) {
            TrieNode node = headNode;
            for (int i = 0; i < prefix.length(); i++) {
                char ch = prefix.charAt(i);
                int index = ch - 'a';
                if (node.children[index] == null)
                    return null;

                node = node.children[index];
            }
            return node;
        }
    }

    // 方法一：（自己写的）前缀树
    class Trie1 {
        Trie1[] children;
        boolean isEnd;

        public Trie1() {
            children = new Trie1[26];
        }

        public void insert(String word) {
            Trie1 trie = this;
            for (char curr : word.toCharArray()) {
                int index = curr - 'a';
                if (trie.children[index] == null)
                    trie.children[index] = new Trie1();

                trie = trie.children[index];
            }
            trie.isEnd = true;
        }

        public boolean search(String word) {
            Trie1 trie = this;
            for (char curr : word.toCharArray()) {
                int index = curr - 'a';
                if (trie.children[index] == null)
                    return false;
                trie = trie.children[index];
            }
            return trie.isEnd;
        }

        public boolean startsWith(String prefix) {
            Trie1 trie = this;
            for (char curr : prefix.toCharArray()) {
                int index = curr - 'a';
                if (trie.children[index] == null)
                    return false;
                trie = trie.children[index];
            }
            return true;
        }
    }

    // 方法二：有序集合-时间复杂度：初始化为O(1)，其余操作为O(log|S|)
    // 空间复杂度：O(n)，字符串的个数
    // （自己的lowb想法：二叉排序树（java的TreeSet），查找前缀：先找插入位置，再比较父节点）
    // 在时间复杂度上，前缀树解法明显优于有序集合解法。
    // 其原因在于：使用前缀树（Trie）的数据结构使得插入、查询全词、查询前缀的时间复杂度与已插入的单词数目无关，
    // 这是前缀树（Trie）解法优于有序集合解法的关键
    class Trie2 {
        private TreeSet<String> treeSet;

        Trie2() {
            treeSet = new TreeSet<>();
        }

        public void insert(String word) {
            treeSet.add(word);
        }

        public boolean search(String word) {
            return treeSet.contains(word);
        }

        public boolean startsWith(String prefix) {
            if (treeSet.contains(prefix))
                return true;

            // 前缀可能包含prefix的字符串一定大于prefix（字符串的比较）
            String high = treeSet.higher(prefix);
            if (high != null && high.startsWith(prefix))
                return true;
            return false;
        }
    }

    // 297.二叉树的序列化与反序列化

}

// 拓扑排序
class TopologicalSortSol {
    // 207. 课程表
}

// 字典树
class DictionaryTreeSol {
    // 139. 单词拆分
    // 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict，判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
    // 说明：
    // 拆分时可以重复使用字典中的单词。
    // 你可以假设字典中没有重复的单词。
    // 注意：该题只需返回是否可以拆分，所以优先考虑动态规划，返回具体拆分方式，则考虑dfs
    // 提示：
    // 1 <= s.length <= 300
    // 1 <= wordDict.length <= 1000
    // 1 <= wordDict[i].length <= 20
    // s 和 wordDict[i] 仅有小写英文字母组成
    // wordDict 中的所有字符串 互不相同

    // 方法一：动态规划-时间复杂度：O(n2)，空间复杂度：O(n)
    // 定义 dp[i] 表示字符串 s 前 i 个字符组成的字符串 s[0..i−1] 是否能被空格拆分成若干个字典中出现的单词（可以是一个也可以是多个）。
    // 0 left right 两部分拼接
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        Set<String> wordDictSet = new HashSet<>(wordDict);// List转Set方便判断contains
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;// 0表示没有单词
        for (int right = 1; right <= n; right++)// 末尾为right（外层）
            for (int left = 0; left < right; left++)// 起始为left（left从头到尾，从尾到头，都可以）
                // 起始字符之前为true（此时接下来拆分的子串才有效），且接下来前闭后开的子串在字典内
                if (dp[left] && wordDictSet.contains(s.substring(left, right))) {// 只要能拆直接break，因为可能还存在其他拆分方法
                    dp[right] = true;
                    break;
                }

        return dp[n];// 字符串末尾为true则能拆分
    }

    // 方法二：回溯法（dfs剪枝） bfs 字典树

    // 208. 实现 Trie (前缀树)
}

// 二叉排序树
class BinarySearchTreeSol {
    // 96.不同的二叉搜索树
    // 98.验证二又搜索树
    // 538.把_叉搜索树转换为累加树

}

// 递归
class RecursionSol {
    class ListNode {
        int val;
        ListNode next;

        ListNode() {
        }

        ListNode(int val) {
            this.val = val;
        }

        ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }

    // 2.两数相加
    // 给你两个 非空 的链表，表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。
    // 请你将两个数相加，并以相同形式返回一个表示和的链表。
    // 你可以假设除了数字 0 之外，这两个数都不会以 0 开头。
    // 提示：
    // 每个链表中的节点数在范围 [1, 100] 内（转换成整数相加不现实！）
    // 0 <= Node.val <= 9
    // 题目数据保证列表表示的数字不含前导零

    // 方法一：模拟-时间复杂度：O(max(m,n))，空间复杂度：O(1)

    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode node = dummyHead;
        int num1, num2, adder = 0;// 进位值
        while (l1 != null || l2 != null) {// 同时为null则结束计算
            num1 = l1 == null ? 0 : l1.val;// 为null则为0
            num2 = l2 == null ? 0 : l2.val;
            int sum = num1 + num2 + adder;
            ListNode cur = new ListNode(sum % 10);
            node.next = cur;
            node = cur;
            adder = sum / 10;
            l1 = l1 == null ? l1 : l1.next;
            l2 = l2 == null ? l2 : l2.next;
        }
        if (adder == 1)// 别忘记处理最高位
            node.next = new ListNode(1);

        return dummyHead.next;
    }

    // 方法二：自己写的lowb递归-时间复杂度：O(max(m,n))，空间复杂度：O(max(m,n))
    ListNode l = null;
    ListNode head = null;

    public ListNode addTwoNumbers2(ListNode l1, ListNode l2) {

        dfs(l1, l2, 0);
        return head;
    }

    void dfs(ListNode l1, ListNode l2, int carry) {
        if (l1 == null && l2 == null) {
            if (carry == 1)
                l.next = new ListNode(1);
            return;
        }

        int n1 = l1 == null ? 0 : l1.val;// 为null则为0
        int n2 = l2 == null ? 0 : l2.val;
        int sum = n1 + n2 + carry;
        carry = sum / 10;

        if (l == null)// 此时为头节点
        {
            l = new ListNode(sum % 10);
            head = l;
        } else {
            l.next = new ListNode(sum % 10);
            l = l.next;
        }

        if (l1 != null)
            l1 = l1.next;

        if (l2 != null)
            l2 = l2.next;

        dfs(l1, l2, carry);
    }

    // 10.正则表达式匹配
    // 给你一个字符串 s 和一个字符规律 p，请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
    // '.' 匹配任意单个字符
    // '*' 匹配零个或多个前面的那一个元素
    // 所谓匹配，是要涵盖 整个 字符串 s的，而不是部分字符串。
    // 提示：
    // 1 <= s.length <= 20
    // 1 <= p.length <= 30
    // s 可能为空，且只包含从 a-z 的小写字母。
    // p 可能为空，且只包含从 a-z 的小写字母，以及字符 . 和 *。
    // 保证每次出现字符 * 时，前面都匹配到有效的字符
    // 理解：ab .* true

    // 方法一：动态规划-时间复杂度、空间复杂度：O(mn)
    // 对匹配的方案进行枚举：
    // f[i][j] 表示 s 的前 i 个字符与 p 中的前 j 个字符是否能够匹配。
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();

        boolean[][] f = new boolean[m + 1][n + 1];
        f[0][0] = true;// 初始化，方便后面的状态转移
        for (int i = 0; i <= m; ++i)// 前 i 个字符
            for (int j = 1; j <= n; ++j) {// 前 j 个字符
                if (p.charAt(j - 1) == '*') {// 第j个字符为*（对应下标j-1）
                    // *前字符匹配 s 末尾的一个字符，将该字符扔掉，而该组合还可以继续进行匹配，
                    // 继承上一状态（看做出现次数+1，或者成功了也不匹配）
                    if (matches(s, p, i, j - 1))
                        f[i][j] = f[i - 1][j] || f[i][j - 2];

                    // *前字符不匹配字符，将该组合扔掉，不再进行匹配，继承上一状态（看做是出现次数为0）
                    else
                        f[i][j] = f[i][j - 2];

                } else {// 第j个字符为字母或.
                    if (matches(s, p, i, j))
                        f[i][j] = f[i - 1][j - 1];// 匹配则继承上一状态（单字符匹配）
                    // 不匹配则为默认值fasle
                }
            }

        return f[m][n];
    }

    // 字符的比较（字符串i长度为0，一定不匹配）
    public boolean matches(String s, String p, int i, int j) {
        if (i == 0)// 字符串长度为0，一定不匹配
            return false;

        if (p.charAt(j - 1) == '.')// 一定匹配
            return true;

        return s.charAt(i - 1) == p.charAt(j - 1);
    }

    // 方法一：（自己写的）动态规划-时间复杂度、空间复杂度：O(mn)
    public boolean isMatch11(String s, String p) {
        int n = s.length();
        int m = p.length();
        boolean dp[][] = new boolean[n + 1][m + 1];
        dp[0][0] = true;
        for (int i = 0; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                char pSign = p.charAt(j - 1);
                if (pSign == '*') {
                    if (i == 0) {
                        dp[i][j] = dp[i][j - 2];
                        continue;
                    }
                    if (match(s.charAt(i - 1), p.charAt(j - 2)))
                        dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
                    else
                        dp[i][j] = dp[i][j - 2];

                } else {
                    if (i == 0)
                        continue;
                    if (match(s.charAt(i - 1), pSign))
                        dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }
        return dp[n][m];
    }

    private boolean match(char s, char p) {
        if (p == '.')
            return true;
        return s == p;
    }

    // 方法二：自己写的lowb递归（dfs回溯，剪枝）-最坏情况复杂度过高
    // 递归，更适合求完整路径（解决方法）的情况
    // 动态规划，更适合只要求返回是否的情况
    String s;
    String p;
    boolean isFlag = false;

    public boolean isMatch2(String s, String p) {
        this.s = s;
        this.p = p;
        dfs(0, 0);
        return isFlag;
    }

    void dfs(int i, int j) {

        if (isFlag)
            return;

        // 判断是否匹配
        if (i == s.length()) {
            if (j == p.length()) {
                isFlag = true;
                return;
            }
            if ((p.length() - j) % 2 == 0) {// 两个一组，格式为?*?*，每组要求出现0次
                for (int starIndex = 1; starIndex < p.length() - j; starIndex = starIndex + 2)
                    if (p.charAt(j + starIndex) != '*')
                        return;

                isFlag = true;
            }
        }

        if (i >= s.length() || j >= p.length())
            return;

        if (j + 1 < p.length()) {// p有后一位
            if (p.charAt(j + 1) == '*') {
                if (matches(i, j)) {
                    dfs(i + 1, j);// 选择重复大于1次
                    dfs(i + 1, j + 2);// 选择重复1次
                }
                dfs(i, j + 2);// 选择重复0次

            } else {// 后一位非*，则j位为字母或.
                if (matches(i, j))
                    dfs(i + 1, j + 1);
            }
        } else {// p最后一位
            if (matches(i, j))
                dfs(i + 1, j + 1);
        }
    }

    boolean matches(int i, int j) {
        if (p.charAt(j) == '.')
            return true;
        return s.charAt(i) == p.charAt(j);
    }

    // 21.合并两个有序链表
    // 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
    // 方法一：递归-时间复杂度、空间复杂度：O(m+n)
    // 改变了原l1 l2的结构
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null)// 只要有一个链表结束，则整个合并结束
            return l2;
        else if (l2 == null)
            return l1;
        else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);// 先传入原l1.next参数，再改变l1.next指向
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }

    // 方法二：迭代-时间复杂度：O(n+m)，空间复杂度：O(1)
    // 改变了原l1 l2的结构
    public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
        ListNode prehead = new ListNode(-1);

        ListNode prev = prehead;// dummy head
        while (l1 != null && l2 != null) {// 只要有一个链表结束，则整个合并结束
            if (l1.val <= l2.val) {
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }

        // 合并后 l1 和 l2 最多只有一个还未被合并完，我们直接将链表末尾指向未合并完的链表即可
        prev.next = l1 == null ? l2 : l1;

        return prehead.next;
    }

    // 206.反转链表
    // 给你单链表的头节点 head ，请你反转链表，并返回反转后的链表。

    // 方法一：迭代-时间复杂度：O(n)，空间复杂度：O(1)
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {// 由curr跳出，需要避免next空指针
            ListNode next = curr.next;// 可看做临时节点，存放curr.next
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }

    // 方法二：递归-时间复杂度：O(n)，空间复杂度：O(n)
    public ListNode reverseList2(ListNode head) {
        if (head == null || head.next == null)// 尾节点开始返回
            return head;
        // ListNode nextNode = head.next; 减少额外的空间开销
        ListNode newHead = reverseList2(head.next);
        head.next.next = head;// 下一个节点的next指向当前节点

        // 当前节点指向null 后续会被更改，直到重新回到原头节点，防止新链表末尾（原链表头）成环
        head.next = null;
        return newHead;
    }

    // 234.回文链表
    // 394.字符串解码

}

// 记忆化搜索（递归）
class MemorySearchSol {
    // 70. 爬楼梯
    // 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
    // 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢？
    // 注意：给定 n 是一个正整数。
    // 利用常规的递归会超出时间限制，递归会有大量重复的计算，记忆化搜索可以减少重复计算

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // f(x) = f(x−1) + f(x−2)
    public int climbStairs(int n) {
        int p = 0, q = 0, r = 1;// 滚动数组，节省内存开销
        for (int i = 1; i <= n; ++i) {
            p = q;
            q = r;
            r = p + q;
        }
        return r;
    }

    // 方法二：矩阵快速幂-时间复杂度：同快速幂，O(logn)，空间复杂度：O(1)
    // f(n) = q^n * [f(1) f(0)]T
    // 快速计算矩阵 q 的 n 次幂，就可以得到 f(n) 的值。如果直接求取 q^n ，时间复杂度是 O(n) ，
    // 我们可以定义矩阵乘法，然后用快速幂算法来加速这里 q^n 的求取

    public int climbStairs2(int n) {
        int[][] q = { { 1, 1 }, { 1, 0 } };
        int[][] res = pow(q, n);
        return res[0][0];
    }

    // 快速幂
    public int[][] pow(int[][] a, int n) {
        int[][] ret = { { 1, 0 }, { 0, 1 } };
        while (n > 0) {
            if ((n & 1) == 1)
                ret = multiply(ret, a);

            n >>= 1;
            a = multiply(a, a);
        }
        return ret;
    }

    // 矩阵乘法
    public int[][] multiply(int[][] a, int[][] b) {
        int[][] c = new int[2][2];
        for (int i = 0; i < 2; i++)
            for (int j = 0; j < 2; j++)
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];

        return c;
    }

    // 方法三：通项公式-时空复杂度与pow函数和CPU 支持的指令集相关，这里不深入分析
    public int climbStairs3(int n) {
        double sqrt5 = Math.sqrt(5);
        double fibn = Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2, n + 1);
        return (int) Math.round(fibn / sqrt5);
    }

    // 139. 单词拆分
}

// 队列
class QueueSol {
    // 239. 滑动窗口最大值
    // 给你一个整数数组 nums，有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。
    // 你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
    // 返回滑动窗口中的最大值。
    // 提示：
    // 1 <= nums.length <= 105
    // -104 <= nums[i] <= 104
    // 1 <= k <= nums.length
    // 窗口大小k可能过大，直接使用max()复杂度O(nk)过高！

    // 方法一：优先队列-时间复杂度：O(nlogn)，将一个元素放入优先队列的时间复杂度为 O(logn)
    // （该题只需要求窗口内最大值，并不关系整个窗口内的排序，使用堆一定程度上做了无用功，同理二叉排序树）
    // 空间复杂度：O(n)
    // 在优先队列中存储二元组 (num,index)，表示元素 num 在数组中的下标为 index
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(
                (pair1, pair2) -> pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1]);

        // 初始时，我们将数组 nums 的前 k 个元素放入优先队列中
        for (int i = 0; i < k; ++i)
            pq.offer(new int[] { nums[i], i });

        int[] ans = new int[n - k + 1];
        ans[0] = pq.peek()[0];// 堆顶即最大值
        for (int i = k; i < n; ++i) {
            pq.offer(new int[] { nums[i], i });// 新元素入队
            while (pq.peek()[1] <= i - k) // 堆顶元素已不在窗口内（要循环判断，直到堆顶元素在窗口内）
                pq.poll();// 出队

            ans[i - k + 1] = pq.peek()[0];// 堆顶即最大值
        }
        return ans;
    }

    // 方法二：单调（双端）队列-时间复杂度：O(n)，空间复杂度：O(k)
    // 单调队列存放索引值，按元素大小严格单调递减
    public int[] maxSlidingWindow2(int[] nums, int k) {
        int n = nums.length;
        Deque<Integer> deque = new LinkedList<Integer>(); // 单调（双端）队列
        // 初始时，我们将数组 nums 的前 k 个元素放入单调队列中
        for (int i = 0; i < k; ++i) {
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()])// 队列非空且当前入队元素比队尾元素大
                deque.pollLast();// 队尾元素的索引出队
            deque.offerLast(i);// 插入至队尾
        }

        int[] ans = new int[n - k + 1];
        ans[0] = nums[deque.peekFirst()];// 队头即最大值
        for (int i = k; i < n; ++i) {
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()])
                deque.pollLast();
            deque.offerLast(i);

            while (deque.peekFirst() <= i - k) // 队头元素已不在窗口内（要循环判断，直到队头元素在窗口内）
                deque.pollFirst();

            ans[i - k + 1] = nums[deque.peekFirst()];// 队头即最大值
        }
        return ans;
    }

    // 方法三：分块 + 预处理-时空复杂度：O(n)
    // 我们可以将数组 nums 从左到右按照 k 个一组进行分组，最后一组中元素的数量可能会不足 k 个。
    // 那么 nums[i] 到 nums[i+k−1] 会跨越两个分组，占有第一个分组的后缀以及第二个分组的前缀。
    // 如果我们能够预处理出每个分组中的前缀最大值以及后缀最大值，同样可以在 O(1) 的时间得到答案。
    // 这种方法与稀疏表（Sparse Table）非常类似
    // 分块边界一定要相对应
    public int[] maxSlidingWindow3(int[] nums, int k) {
        int n = nums.length;
        int[] prefixMax = new int[n];
        int[] suffixMax = new int[n];

        for (int i = 0; i < n; ++i) {
            if (i % k == 0)
                prefixMax[i] = nums[i];
            else
                prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);

        }
        for (int i = n - 1; i >= 0; --i) {
            if (i == n - 1 || (i + 1) % k == 0)
                suffixMax[i] = nums[i];
            else
                suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);

        }

        int[] ans = new int[n - k + 1];
        for (int i = 0; i <= n - k; ++i)
            ans[i] = Math.max(suffixMax[i], prefixMax[i + k - 1]);

        return ans;
    }

}

// 数组
class ArraySol {
    // 1.两数之和
    // 给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 target  的那 两个 整数，并返回它们的数组下标。
    // 你可以假设每种输入只会对应一个答案。但是，数组中同一个元素在答案里不能重复出现。
    // 你可以按任意顺序返回答案。
    // 提示：
    // 2 <= nums.length <= 104
    // -109 <= nums[i] <= 109
    // -109 <= target <= 109
    // 只会存在一个有效答案
    // 你可以想出一个时间复杂度小于 O(n2) 的算法吗？

    // 方法一：暴力枚举-时间复杂度：O(n^2)，空间复杂度：O(1)
    public int[] twoSum(int[] nums, int target) {
        int n = nums.length;
        for (int i = 0; i < n; ++i)
            for (int j = i + 1; j < n; ++j)
                if (nums[i] + nums[j] == target)
                    return new int[] { i, j };

        return new int[0];
    }

    // 方法二：哈希表-时空复杂度：O(n)（空间换取时间）
    public int[] twoSum2(int[] nums, int target) {

        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        // 建表与查表同时进行，因为答案唯一a + b = target，先到a或b都行
        for (int i = 0; i < nums.length; ++i) {
            if (hashtable.containsKey(target - nums[i]))
                return new int[] { hashtable.get(target - nums[i]), i };

            hashtable.put(nums[i], i);
        }
        return new int[0];
    }

    // 4.寻找两个正序数组的中位数
    // 给定两个大小分别为 m 和 n 的正序（从小到大）数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
    // 提示：
    // nums1.length == m
    // nums2.length == n
    // 0 <= m <= 1000
    // 0 <= n <= 1000
    // 1 <= m + n <= 2000
    // -106 <= nums1[i], nums2[i] <= 106
    // 进阶：你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗？
    // 如果对时间复杂度的要求有 log，通常都需要用到二分查找

    // 方法一：二分查找-时间复杂度：O(log(m+n))，空间复杂度：O(1)
    // 这道题可以转化成寻找两个有序数组中的第 k 小的数，其中 k 为 (m+n)/2 或 (m+n)/2+1。
    // 如果 A[k/2−1]<B[k/2−1]，则比A[k/2−1] 小的数最多只有 A 的前 k/2−1 个数和 B 的前 k/2−1 个数，
    // 即比 A[k/2−1] 小的数最多只有 k-2 个，因此 A[k/2−1] 不可能是第 k 个数，
    // A[0] 到 A[k/2−1] 也都不可能是第 k 个数，可以全部排除。
    // 如果 A[k/2−1]>B[k/2−1]，则可以排除 B[0] 到 B[k/2−1]。
    // 如果 A[k/2−1]=B[k/2−1]，则可以归入第一种情况处理。
    // 可以看到，比较 A[k/2−1] 和 B[k/2−1] 之后，可以排除 k/2 个不可能是第 k 小的数，查找范围缩小了一半。
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int length1 = nums1.length, length2 = nums2.length;
        int totalLength = length1 + length2;
        if (totalLength % 2 == 1) {// 总长度为奇数，中位数索引为midIndex
            int midIndex = totalLength / 2;
            double median = getKthElement(nums1, nums2, midIndex + 1);
            return median;
        } else {// 总长度为偶数，中位数为索引midIndex1 + midIndex2的平均值
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1, nums2, midIndex1 + 1) +
                    getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
            return median;
        }
    }

    public int getKthElement(int[] nums1, int[] nums2, int k) {
        // 主要思路：要找到第 k (k>1) 小的元素，
        // 那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
        // 这里的 "/" 表示整除
        // nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
        // nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
        // 取 pivot = min(pivot1, pivot2)，
        // 两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
        // 这样 pivot 本身最大也只能是第 k-1 小的元素
        // 如果 pivot = pivot1，那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。
        // 把这些元素全部 "删除"，剩下的作为新的 nums1 数组
        // 如果 pivot = pivot2，那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。
        // 把这些元素全部 "删除"，剩下的作为新的 nums2 数组
        // 由于我们 "删除" 了一些元素（这些元素都比第 k 小的元素要小），因此需要修改 k 的值，减去删除的数的个数

        int length1 = nums1.length, length2 = nums2.length;
        int index1 = 0, index2 = 0;

        while (true) {
            // 边界情况
            if (index1 == length1)
                return nums2[index2 + k - 1];
            if (index2 == length2)
                return nums1[index1 + k - 1];
            if (k == 1)
                return Math.min(nums1[index1], nums2[index2]);

            // 正常情况
            int half = k / 2;
            int newIndex1 = Math.min(index1 + half, length1) - 1;
            int newIndex2 = Math.min(index2 + half, length2) - 1;
            int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
            if (pivot1 <= pivot2) {// 排除 A[index1] 到 A[k/2−1]
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;// A数组下标移动
            } else {// 排除 B[index2] 到 B[k/2−1]
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;// B数组下标移动
            }
        }
    }

    // 方法一：（自己写的）二分查找-时间复杂度：O(log(m+n))，空间复杂度：O(1)
    public double findMedianSortedArrays11(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int nums1Left = 0, nums2Left = 0;
        int needRemoved = (m + n - 1) / 2;
        while (needRemoved > 0) {
            int removed = Math.max(needRemoved / 2, 1);
            int nums1Mid = nums1Left + removed - 1;
            int nums2Mid = nums2Left + removed - 1;
            int num1 = nums1Mid < m ? nums1[nums1Mid] : Integer.MAX_VALUE;
            int num2 = nums2Mid < n ? nums2[nums2Mid] : Integer.MAX_VALUE;
            if (num1 <= num2)
                nums1Left = nums1Mid + 1;
            else
                nums2Left = nums2Mid + 1;
            needRemoved -= removed;
        }

        PriorityQueue<Integer> pq = new PriorityQueue<>();
        if (nums1Left < m)
            pq.offer(nums1[nums1Left]);
        if (nums1Left + 1 < m)
            pq.offer(nums1[nums1Left + 1]);
        if (nums2Left < n)
            pq.offer(nums2[nums2Left]);
        if (nums2Left + 1 < n)
            pq.offer(nums2[nums2Left + 1]);

        if (((m + n) & 1) == 1)
            return pq.poll();
        else
            return (pq.poll() + pq.poll()) / 2.0;
    }

    // 方法二：划分数组（其实也是二分）-时间复杂度：O(logmin(m,n))，空间复杂度：O(1)
    // 划分思想：中位数，即是将一个集合划分为两个长度相等的子集，其中一个子集中的元素总是大于另一个子集中的元素。
    // nums1 num2分别拆为左右两部分，左右两部分分别合并，得到A, B
    // 当 A 和 B 的总长度是偶数时：
    // 保证A B长度相等，median = (A最大 + B最小) / 2
    // 当 A 和 B 的总长度是奇数时：
    // 保证 len(A) = len(B) + 1，median = A最大
    // i, j 是nums1 num2的拆分点
    // 则：
    // i+j=m−i+n−j（当 m+n 为偶数）或 i+j+1=m−i+n−j（当 m+n 为奇数）二分搜索i值
    // 还要保证：前一部分的最大值小于等于后一部分的最小值
    public double findMedianSortedArrays2(int[] nums1, int[] nums2) {
        // 保证第一个数组长度比第二个数组短(或者相等)，因为二分查找是以nums1的边界作为参考，nums2根据恒等式得到j的值
        // 如果nums1长度大于nums2，那么j可能会计算出为负
        if (nums1.length > nums2.length)
            return findMedianSortedArrays2(nums2, nums1);

        int m = nums1.length;
        int n = nums2.length;
        int left = 0, right = m;
        // median1：前一部分的最大值
        // median2：后一部分的最小值
        int firstHalfMax = 0, secondHalfMin = 0;

        while (left <= right) {
            // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
            int i = (left + right) / 2;// 二分搜索i值
            int j = (m + n) / 2 - i;

            // nums_im1, nums_i, nums_jm1, nums_j
            // 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
            // 用最小最大值充当边界nums[-1] num[m or n]
            int nums_im1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
            int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
            int nums_jm1 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
            int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);

            if (nums_im1 <= nums_j) {// nums1[i-1]<= nums2[j] 这是满足数组划分的一种情况，记录此时的中位数
                firstHalfMax = Math.max(nums_im1, nums_jm1);
                secondHalfMin = Math.min(nums_i, nums_j);
                left = i + 1;
            } else
                right = i - 1;
        }

        return (m + n) % 2 == 0 ? (firstHalfMax + secondHalfMin) / 2.0 : secondHalfMin;
    }

    // 11.盛最多水的容器
    // 实现获取 下一个排列 的函数，算法需要将给定数字序列重新排列成字典序中下一个更大的排列（即，组合出下一个更大的整数）。
    // 如果不存在下一个更大的排列，则将数字重新排列成最小的排列（即升序排列）。
    // 必须 原地 修改，只允许使用额外常数空间。

    // 15.三数之和
    // 给你一个包含 n 个整数的数组 nums，判断 nums 中是否存在三个元素 a，b，c ，使得 a + b + c = 0 ？
    // 请你找出所有和为 0 且不重复的三元组。
    // 注意：答案中不可以包含重复的三元组。
    // 提示：
    // 0 <= nums.length <= 3000
    // -105 <= nums[i] <= 105

    // 方法一：排序 + 双指针（有点牵强）-时间复杂度：O(n^2)，空间复杂度：O(logn)
    // 不包含重复的三元组的关键：需要和上一次枚举的数不相同
    public List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        // a + b + c = 0
        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1])
                continue;

            // c 对应的指针初始指向数组的最右端
            int third = n - 1;
            int target = -nums[first]; // b + c = -a = target
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1])
                    continue;

                // 需要保证 b 的指针在 c 的指针的左侧
                while (second < third && nums[second] + nums[third] > target)
                    --third;

                // 如果指针重合，随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了，可以退出循环
                if (second == third)
                    break;

                // 所得即答案
                if (nums[second] + nums[third] == target) {
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    ans.add(list);
                }
            }
        }
        return ans;
    }

    // 方法一：自己写的（真正的双指针）-时间复杂度：O(n^2)，空间复杂度：O(logn)
    public List<List<Integer>> threeSum11(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1])
                continue;
            int a = nums[i];
            int leftIndex = i + 1;
            int rightIndex = n - 1;
            while (leftIndex < rightIndex) {
                int left = nums[leftIndex];
                int right = nums[rightIndex];
                int sum = a + left + right;
                if (sum == 0) {
                    res.add(new ArrayList<>(Arrays.asList(a, left, right)));
                    // 避免出现重复答案
                    while (leftIndex < n && nums[leftIndex] == left)
                        leftIndex++;
                } else if (sum > 0)
                    rightIndex--;
                else
                    leftIndex++;
            }
        }
        return res;
    }

    // 方法二：排序 + 二分查找-时间复杂度：O(n^2logn)，空间复杂度：O(logn)
    public List<List<Integer>> threeSum2(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();

        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1])
                continue;

            // a + b + c = 0, b + c = -a = target
            int target = -nums[first];
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1])
                    continue;

                // 随着 b 后续的增加，不会有满足 a+b+c=0 并且 b<c 的 c 了，可以退出循环
                if (nums[second] > target / 2)
                    break;

                // 需要保证 b 的指针在 c 的指针的左侧
                int third = Arrays.binarySearch(nums, second + 1, n, target - nums[second]);

                // 没找到当前b对应的c，返回的是 -(应当插入的位置)-1
                if (third < 0)
                    continue;

                // third>=0，则找到了对应的c
                // 所得即答案
                List<Integer> list = Arrays.asList(nums[first], nums[second], nums[third]);// （一般还要放入实现类的构造器中，asList返回的是内部类）
                ans.add(list);
            }
        }
        return ans;
    }

    // 31.下一个排列
    // 实现获取 下一个排列 的函数，算法需要将给定数字序列重新排列成字典序中下一个更大的排列（即，组合出「下一个更大的整数」）。
    // 如果不存在下一个更大的排列，则将数字重新排列成最小的排列（即升序排列）。
    // 必须 原地 修改，只允许使用额外常数空间。

    // 方法一：两遍扫描（贪心？）-时间复杂度：O(n)，空间复杂度：O(1)
    // 思路从最大值 4321 开始，逆序先找到 左边的「较小数」，再找到 右边的「较大数」
    // 我们需要将一个左边的「较小数」与一个右边的「较大数」交换，以能够让当前排列变大，从而得到下一个排列。
    // 同时我们要让这个「较小数」尽量靠右，而「较大数」尽可能小。当交换完成后，「较大数」右边的数需要按照升序重新排列。
    // 这样可以在保证新排列大于原来排列的情况下，使变大的幅度尽可能小。
    // e.g. [4,5,2,6,3,1] （2 3交换） [4,5,3,6,2,1] （621翻转） [4,5,3,1,2,6]
    public void nextPermutation(int[] nums) {
        int i = nums.length - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) // 从后往前，下标i即为第一个非升序数，（较小数）
            i--;

        // i==-1 从后往前都是升序，即不存在下一个更大的排列
        if (i >= 0) {
            int j = nums.length - 1;
            while (j >= 0 && nums[i] >= nums[j])// 从后往前（必升序），找到第一个比下标i大的数j（较大数）
                j--;

            swap(nums, i, j); // 交换左边的「较小数」与右边的「较大数」
        }

        reverse(nums, i + 1);// 「较大数」右边的数需要按照升序重新排列，此时必为降序，翻转即可
    }

    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    // 好家伙，标签里的双指针在这是吧
    public void reverse(int[] nums, int start) {
        int left = start, right = nums.length - 1;
        while (left < right) {
            swap(nums, left, right);
            left++;
            right--;
        }
    }

    // 方法一：自己写的（贪心？）-时间复杂度：O(n)，空间复杂度：O(1)
    public void nextPermutation11(int[] nums) {

        int n = nums.length;
        int leftIndex = n - 2;
        while (leftIndex >= 0 && nums[leftIndex] >= nums[leftIndex + 1])
            leftIndex--;

        if (leftIndex != -1) {
            int rightIndex = 0;
            int index = leftIndex + 1;
            while (index < n && nums[index] > nums[leftIndex]) {
                rightIndex = index;
                index++;
            }
            int temp = nums[leftIndex];
            nums[leftIndex] = nums[rightIndex];
            nums[rightIndex] = temp;
        }
        Arrays.sort(nums, leftIndex + 1, n);// 翻转逆序列
    }

    // 33.搜索旋转排序数组（无重复元素）
    // 整数数组 nums 按升序排列，数组中的值 互不相同 。
    // 在传递给函数之前，nums 在预先未知的某个下标 k（0 <= k < nums.length）上进行了 旋转，
    // 使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ...,
    // nums[k-1]]（下标 从 0 开始 计数）。
    // 例如， [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
    // 给你 旋转后 的数组 nums 和一个整数 target ，如果 nums 中存在这个目标值 target ，则返回它的下标，否则返回 -1 。
    // 提示：
    // 1 <= nums.length <= 5000
    // -10^4 <= nums[i] <= 10^4
    // nums 中的每个值都 独一无二
    // 题目数据保证 nums 在预先未知的某个下标上进行了旋转
    // -10^4 <= target <= 10^4
    // 进阶：你可以设计一个时间复杂度为 O(log n) 的解决方案吗？

    // 方法一：二分查找-时间复杂度：O(logn)，空间复杂度： O(1)
    // 将数组从中间分开成左右两部分的时候，一定有一部分的数组是有序的。
    // 这启示我们可以在常规二分查找的时候，
    // 查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的，
    // 并根据有序的那个部分确定我们该如何改变二分查找的上下界，因为我们能够根据有序的那部分判断出 target 在不在这个部分
    public int search(int[] nums, int target) {
        int n = nums.length;
        if (n == 0)
            return -1;

        if (n == 1)
            return nums[0] == target ? 0 : -1;

        int l = 0, r = n - 1;// 二分法的左右边界
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target)
                return mid;

            // 这里的判断是综合后的，详见自己的写法（完整情况讨论，冗余但易读）
            if (nums[0] <= nums[mid]) {// 左半边有序
                if (nums[0] <= target && target < nums[mid])// target在左半边内，丢弃右半边数据
                    r = mid - 1;
                else// target不在左半边内，丢弃左半边数据
                    l = mid + 1;

            } else {// 右半边有序
                if (nums[mid] < target && target <= nums[n - 1])
                    l = mid + 1;
                else
                    r = mid - 1;
            }
        }
        return -1;// 最后都没找到
    }

    // 方法一：（自己写的，完整情况讨论，冗余但易读）二分查找-时间复杂度：O(logn)，空间复杂度： O(1)
    public int search11(int[] nums, int target) {
        int n = nums.length;
        int left = 0;
        int right = n - 1;
        int firstVal = nums[0];
        // e.g. 4 5 6(t) 7 1 2(t) 3
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target)
                return mid;
            else if (nums[mid] > target) {// 当前值大于target
                if (target >= firstVal) // target在左（当前值大于target），此时情况唯一
                    right = mid - 1;
                else {// target在右边（当前值大于target）
                    if (nums[mid] < firstVal)// 当前值在左边（target在右边，当前值大于target）
                        right = mid - 1;
                    else // 当前值在右边（target在右边，当前值大于target）
                        left = mid + 1;
                }
            } else {// 当前值小于target
                if (target >= firstVal) {// target在左边（当前值小于target）
                    if (nums[mid] < firstVal)// 当前值在左边（target在左边，当前值小于target）
                        right = mid - 1;
                    else // 当前值在左边（target在左边，当前值小于target）
                        left = mid + 1;
                } else // target在右边（当前值小于target），此时情况唯一
                    left = mid + 1;
            }
        }
        return -1;
    }

    // 34.在排序数组中查找元素的第一个和最后一个位置
    // 给定一个按照「升序排列」的整数数组 nums，和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
    // 如果数组中不存在目标值 target，返回 [-1, -1]。
    // 进阶：
    // 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗？

    // 方法一：二分查找-时间复杂度： O(logn) ，空间复杂度：O(1)
    // 寻找 leftIdx 即为在数组中寻找第一个大于 target-1 的数的下标，
    // 寻找 rightIdx 即为在数组中寻找第一个大于 target 的下标，然后将下标减一。
    // 跟一般的二分查找不一样的地方在于：本问题找到target时不返回，只有left = right时才返回
    public int[] searchRange(int[] nums, int target) {
        int leftIdx = searchFirstLarger(nums, target - 1);
        int rightIdx = searchFirstLarger(nums, target) - 1;
        // int leftIdx = searchFirstSmaller(nums, target) + 1;
        // int rightIdx = searchFirstSmaller(nums, target + 1);
        if (leftIdx <= rightIdx && nums[leftIdx] == target)
            return new int[] { leftIdx, rightIdx };

        return new int[] { -1, -1 };
    }

    // 第一个大于 target 的数的下标
    private int searchFirstLarger(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > target)
                right = mid - 1;
            else// nums[mid] <= target时，丢弃左半边
                left = mid + 1;
        }
        return left;
    }

    // 第一个小于 target 的数的下标
    @SuppressWarnings("unused")
    private int searchFirstSmaller(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target)
                right = mid - 1;
            else
                left = mid + 1;
        }
        return right;
    }

    // 39.组合总和
    // 给定一个无重复元素的正整数数组 candidates 和一个正整数 target ，
    // 找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
    // candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同，则两种组合是唯一的。 
    // 对于给定的输入，保证和为 target 的唯一组合数少于 150 个。
    // 提示：
    // 1 <= candidates.length <= 30
    // 1 <= candidates[i] <= 200
    // candidate 中的每个元素都是独一无二的。
    // 1 <= target <= 500
    // 与主站322.零钱兑换不同的是，本题需要返回不重复的具体情况（完整路径），采用搜索回溯（递归）更好，而不是动态规划

    // 方法一：搜索回溯（二叉树，即dfs里调用了2次dfs）-时间复杂度：O(S)，其中 S 为所有可行解的长度之和，空间复杂度：O(target)
    // 使用一般的递归会有重复解出现，此时去重有些麻烦
    // 不重复且不遗漏地找到所有可行解：
    // 递归：1. 跳过某数（后续再也不选择该数），2. 选择某数（后续仍可选择该数），combine中元素排列顺序同candidates[]
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> combine = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 预处理，哪些值是可以组合出来的
        dfs(candidates, target, 0);
        return ans;
    }

    /**
     * 
     * @param candidates
     * @param target
     * @param idx        当前作决定的数的索引
     */
    public void dfs(int[] candidates, int target, int idx) {
        if (idx == candidates.length)
            return;

        if (target == 0) {
            ans.add(new ArrayList<>(combine));
            return;
        }
        // 直接跳过
        dfs(candidates, target, idx + 1);
        // 选择当前数
        if (target - candidates[idx] >= 0) {// 若加上candidates[idx]已超target，则跳过
            combine.add(candidates[idx]);
            dfs(candidates, target - candidates[idx], idx);// 可继续选择该数 candidates[idx]
            combine.remove(combine.size() - 1);// 递归结束后，去除添加的数（还原），调用其他递归
        }
    }

    // 方法一：（自己写的）回溯（0-n叉树）
    // （dfs里调用了0-n次dfs，最好不要这样，代码在运行前就要固定二叉树的分支数）
    public List<List<Integer>> combinationSum11(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> ans = new ArrayList<>();
        dfs(candidates, target, ans, res, 0, 0);
        return res;
    }

    public void dfs(int[] candidates, int target, List<Integer> ans, List<List<Integer>> res, int sum, int index) {
        if (sum == target) {
            res.add(new ArrayList<>(ans));
            return;
        }
        for (int i = index; i < candidates.length; i++) {
            int num = candidates[i];
            if (sum + num <= target) {
                ans.add(num);
                dfs(candidates, target, ans, res, sum + num, i);
                ans.remove(ans.size() - 1);
            }
        }
    }

    // 方法一：（自己写的）回溯（n叉树）
    int sum = 0;
    int target;
    int[] candidates;
    List<Integer> ans39 = new ArrayList<>();

    // List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combinationSum111(int[] candidates, int target) {
        this.target = target;
        this.candidates = candidates;
        dfs(0);
        return res;
    }

    private void dfs(int index) {
        if (sum == target)
            res.add(new ArrayList<>(ans39));

        if (sum > target)
            return;

        for (int i = index; i < candidates.length; i++) {
            int num = candidates[i];
            ans39.add(num);
            sum += num;
            dfs(i);
            ans39.remove(ans39.size() - 1);
            sum -= num;
        }
    }

    // 方法一：（自己写的）回溯
    // 每次确定每一位数字的个数，再在target==0时，构建完整ans放入res
    // int target;
    // int[] candidates;
    int[] count39;

    // List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combinationSum1111(int[] candidates, int target) {
        this.target = target;
        this.candidates = candidates;
        this.count39 = new int[candidates.length];
        dfs39_1111(0);
        return res;
    }

    private void dfs39_1111(int index) {
        if (target == 0) {
            res.add(arrToList(count39));
            return;
        }
        if (index == candidates.length)
            return;

        int num = candidates[index];
        int max = target / num;// 最多选择 num max次
        target -= max * num;
        count39[index] = max;
        dfs(index + 1);
        // 尝试选择 num max-1, max-2,...,0 次
        for (int i = max - 1; i >= 0; i--) {
            count39[index] = i;
            target += num;
            dfs(index + 1);
        }
    }

    private List<Integer> arrToList(int[] arr) {
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < candidates.length; i++)
            for (int j = 0; j < arr[i]; j++)
                ans.add(candidates[i]);
        return ans;
    }

    // 42.接雨水

    // 46.全排列
    // 给定一个不含重复数字的数组 nums ，返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

    // 方法一：回溯 + 交换-时间复杂度：O(n×n!)，空间复杂度：O(n)
    // 核心思想：依次交换位置
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> output = new ArrayList<Integer>();
    int n;

    public List<List<Integer>> permute(int[] nums) {
        for (int num : nums)
            output.add(num);

        n = nums.length;
        backtrack(0);
        return res;
    }

    // 从左往右填到第 first 个位置
    public void backtrack(int first) {
        // 所有数都填完了
        if (first == n)
            res.add(new ArrayList<Integer>(output));

        for (int i = first; i < n; i++) {
            // 动态维护数组
            Collections.swap(output, first, i);// 第一次循环first = i，不交换
            // 继续递归填下一个数
            backtrack(first + 1);
            // 撤销操作
            Collections.swap(output, first, i);
        }
    }

    // 方法二：（自己写的）经典 dfs-时间复杂度：O(n×n!)，空间复杂度：O(n)
    // 还是需要判断哪些数用上了，bfs就不写了（也不太适合）
    class lt100Solition {
        boolean visited[];
        int n;
        int[] nums;
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> ans = new ArrayList<>();

        public List<List<Integer>> permute2(int[] nums) {
            this.nums = nums;
            n = nums.length;
            visited = new boolean[n];
            dfs(0);
            return res;
        }

        private void dfs(int index) {
            if (index == n) {
                res.add(new ArrayList<>(ans));
                return;
            }

            for (int i = 0; i < n; i++) {
                if (!visited[i]) {
                    visited[i] = true;
                    ans.add(nums[i]);
                    dfs(index + 1);
                    visited[i] = false;
                    ans.remove(ans.size() - 1);
                }
            }
        }
    }

    // 48.旋转图像
    // 给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
    // 你必须在 原地 旋转图像，这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
    // 提示：
    // matrix.length == n
    // matrix[i].length == n
    // 1 <= n <= 20
    // -1000 <= matrix[i][j] <= 1000

    // 方法一：原地旋转-时间复杂度：O(n^2)，空间复杂度：O(1)
    // 四数交换为一组
    public void rotate(int[][] matrix) {
        int n = matrix.length;
        for (int i = 0; i < n / 2; ++i)
            for (int j = 0; j < (n + 1) / 2; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - j - 1][i];
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
                matrix[j][n - i - 1] = temp;
            }

    }

    // 方法二：用翻转代替旋转-时间复杂度：O(n^2)，空间复杂度：O(1)
    // 先将其通过水平轴翻转，再根据主对角线翻转，得到答案
    // 先主对角线翻转，则后垂直翻转
    public void rotate2(int[][] matrix) {
        int n = matrix.length;
        // 水平翻转
        for (int i = 0; i < n / 2; ++i)
            for (int j = 0; j < n; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - i - 1][j];
                matrix[n - i - 1][j] = temp;
            }

        // 主对角线翻转
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < i; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
    }

    // 53.最大子序和
    // 给定一个整数数组 nums ，找到一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。
    // 提示：
    // 1 <= nums.length <= 105
    // -104 <= nums[i] <= 104
    // 进阶：如果你已经实现复杂度为 O(n) 的解法，尝试使用更为精妙的 分治法 求解。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // 动态规划转移方程：f(i) = max{f(i−1)+nums[i], nums[i]}
    // 思想：与前者狼狈为奸，或者是自立门户
    public int maxSubArray(int[] nums) {
        int pre = 0, maxAns = nums[0];
        for (int x : nums) {
            pre = Math.max(pre + x, x);
            maxAns = Math.max(maxAns, pre);
        }
        return maxAns;
    }

    // 方法二：分治（线段树）-时间复杂度：O(n)，空间复杂度：O(logn)
    // 取 m = (l+r)/2，对区间 [l,m] 和 [m+1,r] 分治求解
    // 对于一个区间 [l,r]，我们可以维护四个量：
    // lSum 表示 [l,r] 内以 l 为左端点的最大子段和
    // rSum 表示 [l,r] 内以 r 为右端点的最大子段和
    // iSum 表示 [l,r] 的区间和
    // mSum 表示 [l,r] 内的最大子段和
    // 考虑 [l,r] 的 mSum 对应的区间是否跨越 m

    // 方法二的意义：
    // 线段树不仅可以解决区间 [0, n-1]，还可以用于解决任意的子区间 [l,r] 的问题。
    // 如果我们把 [0, n-1] 分治下去出现的所有子区间的信息都用堆式存储的方式记忆化下来，
    // 即建成一颗真正的树之后，我们就可以在 O(logn) 的时间内求到任意区间内的答案，
    // 我们甚至可以修改序列中的值，做一些简单的维护，之后仍然可以在 O(logn) 的时间内求到任意区间内的答案，
    // 对于大规模查询的情况下，这种方法的优势便体现了出来。

    public class Status {
        public int lSum, rSum, mSum, iSum;

        public Status(int lSum, int rSum, int mSum, int iSum) {
            this.lSum = lSum;
            this.rSum = rSum;
            this.mSum = mSum;
            this.iSum = iSum;
        }
    }

    public int maxSubArray2(int[] nums) {
        return getInfo(nums, 0, nums.length - 1).mSum;
    }

    public Status getInfo(int[] a, int l, int r) {
        if (l == r)// 分到只剩一个数
            return new Status(a[l], a[l], a[l], a[l]);

        int m = (l + r) >> 1;
        // 分为左右区间
        Status lSub = getInfo(a, l, m);
        Status rSub = getInfo(a, m + 1, r);
        return pushUp(lSub, rSub);
    }

    // 合并
    public Status pushUp(Status l, Status r) {
        // iSum 表示 [l,r] 的区间和
        int iSum = l.iSum + r.iSum;

        // lSum 表示 [l,r] 内以 l 为左端点的最大子段和
        int lSum = Math.max(l.lSum, l.iSum + r.lSum);

        // rSum 表示 [l,r] 内以 r 为右端点的最大子段和
        int rSum = Math.max(r.rSum, r.iSum + l.rSum);

        // mSum 表示 [l,r] 内的最大子段和（左：不横跨，右：横跨）
        int mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum);
        return new Status(lSum, rSum, mSum, iSum);
    }

    // 55.跳跃游戏

    // 56.合并区间
    // 以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。
    // 请你合并所有重叠的区间，并返回一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间。
    // 示例 1：
    // 输入：intervals = [[1,3],[2,6],[8,10],[15,18]] （数组不一定按大小顺序排列）
    // 输出：[[1,6],[8,10],[15,18]]
    // 解释：区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]
    // 提示：
    // 1 <= intervals.length <= 104
    // intervals[i].length == 2
    // 0 <= starti <= endi <= 104

    // 方法一：排序 + 栈-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0)
            return new int[0][2];

        Comparator<int[]> c = (interval1, interval2) -> interval1[0] - interval2[0];// 以start升序排列

        Arrays.sort(intervals, c);
        // 用数组 merged 存储最终的答案
        Deque<int[]> merged = new LinkedList<int[]>();
        for (int i = 0; i < intervals.length; ++i) {
            int L = intervals[i][0], R = intervals[i][1];
            // merged数组中还无数组，直接加入（初始情况）；或者merged末尾二元组的R小于L（不能合并），加入
            if (merged.size() == 0 || merged.peek()[1] < L)
                merged.push(new int[] { L, R });
            else {// merged数组末尾大于L（能合并），更新merged末尾二元组的R
                int[] lastInterval = merged.pop();
                merged.push(new int[] { lastInterval[0], Math.max(lastInterval[1], R) });
            }
        }
        return merged.toArray(new int[merged.size()][]);
    }

    // 64.最小路径和
    // 给定一个包含非负整数的 m x n 网格 grid ，请找出一条从左上角到右下角的路径，使得路径上的数字总和为最小。
    // 说明：每次只能向下或者向右移动一步。
    // 提示：
    // m == grid.length
    // n == grid[i].length
    // 1 <= m, n <= 200
    // 0 <= grid[i][j] <= 100

    // 方法一：动态规划（优化内存开销，每次只存储上一行的 dp 值）-时间复杂度：O(mn)，空间复杂度：O(n)
    public int minPathSum2(int[][] grid) {
        if (grid == null || grid[0].length == 0)
            return 0;

        int rows = grid.length, columns = grid[0].length;
        int[] dp = new int[columns];
        dp[0] = grid[0][0];

        // 计算第一行
        for (int column = 1; column < columns; column++)
            dp[column] = dp[column - 1] + grid[0][column];

        for (int i = 1; i < rows; i++) {
            dp[0] = dp[0] + grid[i][0];// 单独处理每行第一个元素
            for (int j = 1; j < columns; j++)
                dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
        }

        return dp[columns - 1];
    }

    // 方法二：dfs（自己的lowb想法）
    // 直接从头到尾，中间点会有大量重复计算，考虑记忆化搜索，每个中间点都存在最优，将dfs每步变小，每步只向右向左，即动态规划

    // 75.颜色分类（「荷兰国旗问题」）
    // 给定一个包含红色、白色和蓝色，一共 n 个元素的数组，原地对它们进行排序，使得相同颜色的元素相邻，并按照红色、白色、蓝色顺序排列。
    // 此题中，我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
    // 示例 1：
    // 输入：nums = [2,0,2,1,1,0]
    // 输出：[0,0,1,1,2,2]
    // 示例 2：
    // 输入：nums = [2,0,1]
    // 输出：[0,1,2]
    // 进阶：
    // 你可以不使用代码库中的排序函数(nlogn)来解决这道题吗？
    // 你能想出一个仅使用常数空间的一趟扫描算法吗？

    // 方法一：单指针-时间复杂度：O(n)，空间复杂度：O(1)
    // 考虑对数组进行两次遍历。
    // 在第一次遍历中，我们将数组中所有的 0 交换到数组的头部
    // 在第二次遍历中，我们将数组中所有的 1 交换到头部的 0 之后（将数组中所有的 2 交换到数组的尾部。）
    public void sortColors(int[] nums) {
        int n = nums.length;
        int ptr = 0;
        // 在第一次遍历
        for (int i = 0; i < n; ++i) {
            if (nums[i] == 0) {
                int temp = nums[i];
                nums[i] = nums[ptr];
                nums[ptr] = temp;
                ++ptr;
            }
        }

        // 在第二次遍历
        for (int i = ptr; i < n; ++i) {
            if (nums[i] == 1) {
                int temp = nums[i];
                nums[i] = nums[ptr];
                nums[ptr] = temp;
                ++ptr;
            }
        }
    }

    // 方法二：双指针（三指针？指向0 1 跳2）-时间复杂度：O(n)，空间复杂度：O(1)
    // 方法一需要进行两次遍历，那么我们是否可以仅使用一次遍历呢？我们可以额外使用一个指针，即使用两个指针分别用来交换 0 和 1。
    // 从左向右遍历整个数组，遍历到 2 不作处理
    public void sortColors2(int[] nums) {
        int n = nums.length;
        // 用指针 p0 来交换 0，p1 来交换 1，初始索引都为 0。
        int p0 = 0, p1 = 0;
        for (int i = 0; i < n; ++i) {
            if (nums[i] == 1) {
                swap(nums, i, p1);
                // 只有p1右移
                ++p1;
            } else if (nums[i] == 0) {
                swap(nums, i, p0);
                // nums[i]==1，继续交换
                if (nums[i] == 1)
                    swap(nums, i, p1);

                // 都右移
                ++p0;
                ++p1;
            }
        }
    }

    // 方法三：双指针（三指针？指向0 2 跳1）-时间复杂度：O(n)，空间复杂度：O(1)
    // 考虑使用指针 p0 来交换 0，p2 来交换 2。此时，p0 的初始索引仍然为 0，而 p2 的初始索引为 n−1
    // 在遍历的过程中，我们需要找出所有的 0 交换至数组的头部，并且找出所有的 2 交换至数组的尾部。
    // 遍历到 1 不作处理
    public void sortColors3(int[] nums) {
        int n = nums.length;
        int p0 = 0, p2 = n - 1;
        // 如果遍历到的位置超过了 p2，那么就可以直接停止遍历了
        for (int i = 0; i <= p2; ++i) {
            // 当我们找到 2 时，我们需要不断地将其与 nums[p2] 进行交换，直到新的 nums[i] 不为 2。
            // 此时，如果 nums[i] 为 0，那么对应着第一种情况；如果 nums[i] 为 1，那么就不需要进行任何后续的操作。
            while (i <= p2 && nums[i] == 2) {
                swap(nums, i, p2);
                --p2;
            }

            // 注意处理 2 0 的先后顺序
            if (nums[i] == 0) {
                swap(nums, i, p0);
                ++p0;
            }
        }
    }

    // private void swap(int[] nums, int i, int j){
    // int temp = nums[i];
    // nums[i] = nums[j];
    // nums[j] = temp;
    // }

    // 78.子集

    // 79.单词搜索
    // 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中，返回 true ；否则，返回 false 。
    // 单词必须按照字母顺序，通过相邻的单元格内的字母构成，其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。
    // 同一个单元格内的字母不允许被重复使用。
    // 提示：
    // m == board.length
    // n = board[i].length
    // 1 <= m, n <= 6
    // 1 <= word.length <= 15
    // board 和 word 仅由大小写英文字母组成
    // 进阶：你可以使用搜索剪枝的技术来优化解决方案，使其在 board 更大的情况下可以更快解决问题？

    // 方法一：回溯-时间复杂度：O(MN 3^L)（实际远小于此），空间复杂度：O(MN)
    char[][] myBoard;
    String myWord;
    // 标记是否被访问过
    boolean[][] visited;

    public boolean exist(char[][] board, String word) {
        int h = board.length, w = board[0].length;
        myBoard = board;
        myWord = word;
        visited = new boolean[h][w];
        for (int i = 0; i < h; i++)
            for (int j = 0; j < w; j++)
                if (check(i, j, 0))
                    return true;

        return false;
    }

    // 当前坐标i j，当前递归到的字符位置k
    public boolean check(int i, int j, int k) {
        if (myBoard[i][j] != myWord.charAt(k))
            return false;
        else if (k == myWord.length() - 1)
            return true;

        visited[i][j] = true;
        // 上下左右四个方向（实际最多三个有可能）
        int[][] directions = { { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 } };

        for (int[] dir : directions) {
            // 下一步
            int newi = i + dir[0], newj = j + dir[1];
            // 判断是否越界
            if (newi >= 0 && newi < myBoard.length && newj >= 0 && newj < myBoard[0].length)
                // 判断是否访问过
                if (!visited[newi][newj])
                    if (check(newi, newj, k + 1))
                        return true;

        }
        // 结束递归时还原
        visited[i][j] = false;
        return false;
    }

    // 84.柱状图中最大的矩形
    // 85.最大矩形
    // 105.从前序与中序遍历序列构造二叉树

    // 121.买卖股票的最佳时机
    // 给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
    // 你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
    // 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。
    // 提示：
    // 1 <= prices.length <= 105
    // 0 <= prices[i] <= 104

    // 方法一：暴力法-时间复杂度：O(n^2)，空间复杂度：O(1)

    // 方法二：一次遍历（应该不算动规，算贪心吧）-时间复杂度：O(n)，空间复杂度：O(1)
    public int maxProfit(int prices[]) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minprice)// 更新最小价
                minprice = prices[i];
            else if (prices[i] - minprice > maxprofit)// 更新最大收益
                maxprofit = prices[i] - minprice;

        }
        return maxprofit;
    }

    // 128.最长连续序列
    // 136.只出现一次的数字

    // 152.乘积最大子数组（类比53.最大子序和，思想：与前者狼狈为奸，或者是自立门户）
    // 给你一个整数数组 nums ，请你找出数组中乘积最大的连续子数组（该子数组中至少包含一个数字），并返回该子数组所对应的乘积。
    // 不能用前缀和，中间出现0，则后续前缀乘积都为0

    // 方法一：动态规划-时间复杂度为 O(n)，空间复杂度为 O(1)
    // 根据正负性进行分类讨论。
    // 考虑当前位置如果是一个负数的话，那么我们希望以它前一个位置结尾的某个段的积也是个负数，
    // 这样就可以负负得正，并且我们希望这个积尽可能「负得更多」，即尽可能小。
    // 如果当前位置是一个正数的话，我们更希望以它前一个位置结尾的某个段的积也是个正数，并且希望它尽可能地大。
    public int maxProduct(int[] nums) {
        // minF表示以第 i 个元素结尾的乘积最小子数组的乘积
        // maxF表示以第 i 个元素结尾的乘积最大子数组的乘积
        // 由于第 i 个状态只和第 i - 1 个状态相关，根据「滚动数组」思想，我们可以只用两个变量来维护 i - 1 时刻的状态
        int maxF = nums[0], minF = nums[0], ans = nums[0];
        int length = nums.length;
        for (int i = 1; i < length; ++i) {
            int num = nums[i];
            int mx = maxF, mn = minF;
            maxF = Math.max(mx * num, Math.max(num, mn * num));// 尽可能大
            minF = Math.min(mx * num, Math.min(num, mn * num));// 尽可能小
            ans = Math.max(maxF, ans);
        }
        return ans;
    }

    // 方法二：分治（线段树）-时间复杂度：O(n)，空间复杂度：O(logn)
    // 需同时维护正负的最大值

    // 169.多数元素
    // 给定一个大小为 n 的数组，找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
    // 你可以假设数组是非空的，并且给定的数组总是存在多数元素。
    // 进阶：
    // 尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

    // 方法一：哈希表-时间复杂度：O(n)，空间复杂度：O(n)

    // 方法二：排序（下标为 ⌊ n/2 ⌋ 的元素一定是众数）-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 如果使用语言自带的排序算法，需要使用 O(logn) 的栈空间。
    // 如果自己编写堆排序，则只需要使用 O(1) 的额外空间。

    // 方法三：随机化-时间复杂度为 O(n)，空间复杂度：O(1)
    // 随机挑选一个下标，检查它是否是众数，如果是就返回，否则继续随机挑选。
    private int randRange(Random rand, int min, int max) {
        return rand.nextInt(max - min) + min;
    }

    private int countOccurences(int[] nums, int num) {
        int count = 0;
        for (int i = 0; i < nums.length; i++)
            if (nums[i] == num)
                count++;

        return count;
    }

    public int majorityElement3(int[] nums) {
        Random rand = new Random();
        int majorityCount = nums.length / 2;// 多数元素至少出现的次数

        while (true) {
            int candidate = nums[randRange(rand, 0, nums.length)];// 随机取一个数
            if (countOccurences(nums, candidate) > majorityCount) // 验证该数出现的次数
                return candidate;
        }
    }

    // 方法四：分治-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 使用经典的分治算法递归求解，直到所有的子问题都是长度为 1 的数组。
    // 长度为 1 的子数组中唯一的数显然是众数，直接返回即可。
    // 如果回溯后某区间的长度大于 1，我们必须将左右子区间的值合并。
    // 如果它们的众数相同，那么显然这一段区间的众数是它们相同的值。
    // 否则，我们需要比较两个众数在整个区间内出现的次数来决定该区间的众数。
    private int countInRange(int[] nums, int num, int lo, int hi) {
        int count = 0;
        for (int i = lo; i <= hi; i++)
            if (nums[i] == num)
                count++;

        return count;
    }

    private int majorityElementRec(int[] nums, int lo, int hi) {
        // 长度为 1 的子数组中唯一的数显然是众数，直接返回即可。
        if (lo == hi)
            return nums[lo];

        // 将区间一分为二
        int mid = (hi + lo) / 2;
        int left = majorityElementRec(nums, lo, mid);
        int right = majorityElementRec(nums, mid + 1, hi);

        // 左右子数组众数一致
        if (left == right)
            return left;

        // 左右子数组众数不一致，返回更多者
        int leftCount = countInRange(nums, left, lo, hi);
        int rightCount = countInRange(nums, right, lo, hi);

        return leftCount > rightCount ? left : right;
    }

    public int majorityElement4(int[] nums) {
        return majorityElementRec(nums, 0, nums.length - 1);
    }

    // 方法五：Boyer-Moore 摩尔投票算法-时间复杂度为 O(n)，空间复杂度：O(1)
    // 如果我们把众数记为 +1，把其他数记为 −1，将它们全部加起来，显然和大于 0，从结果本身我们可以看出众数比其他数多。
    // 我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值，count 为 0；
    // 我们遍历数组 nums 中的所有元素，对于每个元素 x，在判断 x 之前，如果 count 的值为 0，我们先将 x 的值赋予 candidate，
    // 随后我们判断 x：
    // 如果 x 与 candidate 相等，那么计数器 count 的值增加 1；
    // 如果 x 与 candidate 不等，那么计数器 count 的值减少 1。
    // 在遍历完成后，candidate 即为整个数组的众数。
    public int majorityElement5(int[] nums) {
        int count = 0;
        int candidate = -1;

        for (int num : nums) {
            if (count == 0)
                candidate = num;

            count += (num == candidate) ? 1 : -1;
        }

        return candidate;
    }

    // 198.打家劫舍（数组，打家劫舍 III，树）
    // 你是一个专业的小偷，计划偷窃沿街的房屋。
    // 每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，
    // 如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。
    // 给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // 如果房屋数量大于两间，应该如何计算能够偷窃到的最高总金额呢？对于第 k (k>2) 间房屋，有两个选项：
    // 偷窃第 k 间房屋，那么就不能偷窃第 k-1 间房屋，偷窃总金额为前 k-2 间房屋的最高总金额与第 k 间房屋的金额之和。
    // 不偷窃第 k 间房屋，偷窃总金额为前 k-1 间房屋的最高总金额。
    // 在两个选项中选择偷窃总金额较大的选项，该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。
    // dp[i] 表示前 i 间房屋能偷窃到的最高总金额，那么就有如下的状态转移方程：
    // dp[i]=max(dp[i−2]+nums[i],dp[i−1])
    public int rob(int[] nums) {
        int length = nums.length;

        if (length == 1)
            return nums[0];

        // 考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关，因此可以使用滚动数组，在每个时刻只需要存储前两间房屋的最高总金额。
        int first = nums[0], second = Math.max(nums[0], nums[1]);
        for (int i = 2; i < length; i++) {
            int temp = second;
            second = Math.max(first + nums[i], second);
            first = temp;
        }
        return second;
    }

    // 方法二：动态规划（自己写的）-时间复杂度：O(n)，空间复杂度：O(1)
    public int rob2(int[] nums) {
        int dp0 = 0;// 不偷当前的最高总额
        int dp1 = nums[0];// 偷当前的最高总额
        for (int i = 1; i < nums.length; i++) {
            int num = nums[i];
            int newdp0 = Math.max(dp0, dp1);
            int newdp1 = dp0 + num;
            dp0 = newdp0;
            dp1 = newdp1;
        }
        return Math.max(dp0, dp1);
    }

    // 200.岛屿数量

    // 215.数组中的第K个最大元素
    // 给定整数数组 nums 和整数 k，请返回数组中第 k 个最大的元素。
    // 请注意，你需要找的是数组排序后的第 k 个最大的元素，而不是第 k 个不同的元素。
    // 提示：
    // 1 <= k <= nums.length <= 104
    // -104 <= nums[i] <= 104

    // 方法一：基于快速排序的选择方法-时间复杂度：O(n)，空间复杂度：O(logn)
    // 可以用快速排序来解决这个问题，先对原数组排序，再返回倒数第 k 个位置，这样平均时间复杂度是 O(nlogn)，但其实我们可以做的更快。
    // 改进快速排序算法来解决这个问题：
    // 在分解的过程当中，我们会对子数组进行划分，如果划分得到的 q 正好就是我们需要的下标，就直接返回 a[q]；
    // 否则，如果 q 比目标下标小，就递归右子区间，否则递归左子区间。
    // 这样就可以把原来递归两个区间变成只递归一个区间，提高了时间效率。这就是「快速选择」算法。

    // 引入随机化来加速这个过程，它的时间代价的期望是 O(n)
    public int findKthLargest(int[] nums, int k) {
        quickSort(nums, k - 1, 0, nums.length - 1);// 第k大转换为相应下标
        return nums[k - 1];
    }

    private void quickSort(int[] nums, int index, int left, int right) {
        int pivotIndex = partition(nums, left, right);
        if (pivotIndex == index) // pivot最终位置的下标就是要找的
            return;

        if (pivotIndex < index) // 根据结果只选择一边继续查找
            quickSort(nums, index, pivotIndex + 1, right);
        else
            quickSort(nums, index, left, pivotIndex - 1);

    }

    private int partition(int[] nums, int left, int right) {
        Random random = new Random();
        int pivotIndex = random.nextInt(right - left + 1) + left;// 在区间left right之间随机选择一个pivot
        int pivot = nums[pivotIndex];
        swap(nums, pivotIndex, right);// pivot放到最右边去当哨兵
        int index = left;// 交换位
        for (int i = left; i < right; i++) // 索引right上是pivot
            if (nums[i] > pivot) {// 大于pivot的数放在交换位上，即大于pivot的数在左边，降序排列，同理升序（小的放左边）
                swap(nums, i, index);
                index++;// 交换位右移一位
            }

        swap(nums, index, right);// pivot放到最终位置上
        return index;

    }

    // private void swap(int[] nums, int index1, int index2){
    // int temp = nums[index1];
    // nums[index1] = nums[index2];
    // nums[index2] = temp;
    // }

    // 方法二：基于堆排序的选择方法（不借助优先队列API）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 索引的处理像个猪比，看（自己写的）
    // 也可以使用堆排序来解决这个问题——建立一个大根堆（数组映射到树），做 k - 1 次删除操作后堆顶元素就是我们要找的答案。
    int[] myNums;

    public int findKthLargest2(int[] nums, int k) {
        myNums = nums;
        int heapSize = nums.length;
        buildMaxHeap(heapSize);// 建立大根堆
        for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {// i始终指向末尾叶子节点
            swap(0, i);// 交换最大值与末尾叶子节点
            --heapSize;// 等价于删除最大值
            maxHeapify(0, heapSize);// 在指定范围内，将以i为根的子树调整为大根堆
        }
        return nums[0];
    }

    // 建立大根堆
    public void buildMaxHeap(int heapSize) {
        // 对于所有分支节点（节点数向下取整，非叶子节点），从后往前依次调整
        // 不能从前往后，要先处理底层，使可能沉底的最大元素逐层上浮直至最顶
        for (int i = heapSize / 2; i >= 0; --i)
            maxHeapify(i, heapSize);
    }

    // 在指定范围内，将以i为根的子树调整为大根堆（小元素下沉）
    public void maxHeapify(int root, int heapSize) {
        int l = root * 2 + 1, r = root * 2 + 2;// root节点的左右孩子
        int largest = root;
        if (l < heapSize && myNums[l] > myNums[largest])// 左孩子处于范围内且左孩子更大
            largest = l;

        if (r < heapSize && myNums[r] > myNums[largest])// 右孩子处于范围内且右孩子最大
            largest = r;

        if (largest != root) {// root不是最大的
            swap(root, largest);// 交换与最大节点
            maxHeapify(largest, heapSize);// 递归调整
        }
    }

    public void swap(int i, int j) {
        int temp = myNums[i];
        myNums[i] = myNums[j];
        myNums[j] = temp;
    }

    // 方法二：（自己写的）实现大根堆-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 不在原地建堆，而是复制到新的数组中建堆，下标1开始，方便计算完全二叉树的节点编号
    int[] heap;
    int size;

    // int n;
    public int findKthLargest22(int[] nums, int k) {
        n = nums.length;
        // 1 <= k <= nums.length <= 104 初值为0，不影响堆
        // 填充节点，简化最后一个非叶子节点的操作
        heap = new int[n + 2];
        for (int i = 0; i < n; i++) {
            heap[i + 1] = nums[i];
            size++;
        }

        for (int i = size / 2; i >= 1; i--) // 注意顺序！一定是非叶子节点，从后往前！
            buildHeap(i);

        for (int i = 1; i <= k - 1; i++) {// 出堆k-1个元素，此时堆顶即第k大的元素
            // 最后一个节点放到堆顶，并做下沉操作
            heap[1] = heap[size];
            size--;
            buildHeap(1);
        }
        return heap[1];
    }

    private void buildHeap(int rootIndex) {
        if (rootIndex > size / 2)// 不操作叶子节点
            return;
        int leftIndex = rootIndex * 2;
        int rightIndex = rootIndex * 2 + 1;

        int root = heap[rootIndex];
        int left = heap[leftIndex];
        int right = heap[rightIndex];

        if (root >= left && root >= right)// 已满足堆的性质，无需操作
            return;

        if (left >= right) {
            swap(leftIndex, rootIndex);
            buildHeap(leftIndex);
        } else {
            swap(rightIndex, rootIndex);
            buildHeap(rightIndex);
        }
    }

    // private void swap(int i, int j){
    // int temp = heap[i];
    // heap[i] = heap[j];
    // heap[j] = temp;
    // }

    // 方法三：利用javaAPI的做快排Arrays.sort()，堆排PriorityQueue
    // -时间复杂度：O(nlogk)，空间复杂度：O(k)
    public int findKthLargest3(int[] nums, int k) {
        // 长度为k的小根堆
        PriorityQueue<Integer> pq = new PriorityQueue<>(k, (num1, num2) -> num1 - num2);
        for (int num : nums)
            if (pq.size() < k)
                pq.offer(num);
            else {// 优先队列长度为k，更新最小值
                if (num > pq.peek()) {
                    pq.poll();
                    pq.offer(num);
                }
            }

        return pq.peek();
    }

    // 221.最大正方形
    // 在一个由 '0' 和 '1' 组成的二维矩阵内，找到只包含 '1' 的最大正方形，并返回其面积。
    // 提示：
    // m == matrix.length
    // n == matrix[i].length
    // 1 <= m, n <= 300
    // matrix[i][j] 为 '0' 或 '1' 注意是char数组

    // 方法一：暴力法

    // 方法二：动态规划-时间复杂度：O(n^2)，空间复杂度：O(n^2)
    // 如果该位置的值是 0，则 dp(i,j)=0，因为当前位置不可能在由 1 组成的正方形中；
    // 如果该位置的值是 1，则 dp(i,j) 的值由其上方、左方和左上方的三个相邻位置的 dp 值决定。
    // 具体而言，当前位置的元素值等于三个相邻位置的元素中的最小值加 1，
    // 状态转移方程如下：
    // dp(i, j)=min(dp(i−1, j), dp(i−1, j−1), dp(i, j−1))+1
    public int maximalSquare(char[][] matrix) {
        int maxSide = 0;
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
            return maxSide;

        int rows = matrix.length, columns = matrix[0].length;
        int[][] dp = new int[rows][columns];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; j++) {
                if (matrix[i][j] == '1') {
                    if (i == 0 || j == 0)
                        dp[i][j] = 1;
                    else
                        dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;

                    maxSide = Math.max(maxSide, dp[i][j]);
                }
            }
        }
        int maxSquare = maxSide * maxSide;
        return maxSquare;
    }

    // 238.除自身以外数组的乘积
    // 给你一个长度为 n 的整数数组 nums，其中 n > 1，返回输出数组 output ，
    // 其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
    // 提示：题目数据保证数组之中任意元素的全部前缀元素和后缀（甚至是整个数组）的乘积都在 32 位整数范围内。
    // 说明: 请「不要使用除法」，且在 O(n) 时间复杂度内完成此题。
    // 进阶：
    // 你可以在常数空间复杂度内完成这个题目吗？（ 出于对空间复杂度分析的目的，输出数组不被视为额外空间。）

    // 方法一：左右乘积列表-时间复杂度：O(n)，空间复杂度：O(n)
    // 数组 L 和 R。对于给定索引 i，L[i] 代表的是 i 左侧所有数字的乘积，R[i] 代表的是 i 右侧所有数字的乘积。
    public int[] productExceptSelf(int[] nums) {
        int length = nums.length;

        // L 和 R 分别表示左右两侧的乘积列表
        int[] L = new int[length];
        int[] R = new int[length];
        int[] answer = new int[length];

        // L[i] 为索引 i 左侧所有元素的乘积
        // 对于索引为 '0' 的元素，因为左侧没有元素，所以 L[0] = 1
        L[0] = 1;
        for (int i = 1; i < length; i++)
            L[i] = nums[i - 1] * L[i - 1];

        // R[i] 为索引 i 右侧所有元素的乘积
        // 对于索引为 'length-1' 的元素，因为右侧没有元素，所以 R[length-1] = 1
        R[length - 1] = 1;
        for (int i = length - 2; i >= 0; i--)
            R[i] = nums[i + 1] * R[i + 1];

        // 对于索引 i，除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积
        for (int i = 0; i < length; i++)
            answer[i] = L[i] * R[i];

        return answer;
    }

    // 方法二：找规律-时间复杂度：O(n)，空间复杂度：O(1)
    // 先把输出数组当作 L 数组来计算，然后再动态构造 R 数组得到结果
    public int[] productExceptSelf2(int[] nums) {
        int length = nums.length;
        int[] answer = new int[length];

        // answer[i] 表示索引 i 左侧所有元素的乘积
        // 因为索引为 '0' 的元素左侧没有元素， 所以 answer[0] = 1
        answer[0] = 1;
        for (int i = 1; i < length; i++)
            answer[i] = nums[i - 1] * answer[i - 1];

        // R 为右侧所有元素的乘积
        // 刚开始右边没有元素，所以 R = 1
        int R = 1;
        for (int i = length - 1; i >= 0; i--) {
            // 对于索引 i，左边的乘积为 answer[i]，右边的乘积为 R
            answer[i] = answer[i] * R;
            // R 需要包含右边所有的乘积，所以计算下一个结果时需要将当前值乘到 R 上
            R *= nums[i];
        }
        return answer;
    }

    // 239.滑动窗口最大值

    // 240.搜索二维矩阵II
    // 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性：
    // 每行的元素从左到右升序排列。
    // 每列的元素从上到下升序排列。
    // 提示：
    // m == matrix.length
    // n == matrix[i].length
    // 1 <= n, m <= 300
    // -109 <= matrix[i][j] <= 109
    // -109 <= target <= 109

    // 方法一：直接查找-时间复杂度：O(mn)
    // 方法二：二分查找-时间复杂度：O(mlogn)
    // 由于矩阵 matrix 中每一行的元素都是升序排列的，因此我们可以对每一行都使用一次二分查找
    public boolean searchMatrix2(int[][] matrix, int target) {
        for (int[] row : matrix) {
            int index = biSearch(row, target);
            if (index >= 0)
                return true;

        }
        return false;
    }

    public int biSearch(int[] nums, int target) {
        int low = 0, high = nums.length - 1;
        while (low <= high) {
            int mid = (high - low) / 2 + low;
            int num = nums[mid];
            if (num == target)
                return mid;
            else if (num > target)
                high = mid - 1;
            else
                low = mid + 1;

        }
        return -1;
    }

    // 方法三：Z 字形查找-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 从矩阵 matrix 的右上角 (0, n-1) 进行搜索。在每一步的搜索过程中，如果我们位于位置 (x, y)，
    // 那么我们希望在以 matrix 的左下角为左下角、以 (x, y) 为右上角的矩阵中进行搜索，
    // 即行的范围为 [x, m - 1]，列的范围为 [0, y]：
    public boolean searchMatrix3(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        int x = 0, y = n - 1;
        while (x < m && y >= 0) {
            if (matrix[x][y] == target)
                return true;

            if (matrix[x][y] > target)// matrix[x][y]往下也一定大于target，丢弃该列
                --y;
            else// matrix[x][y]往左一定小于target，丢弃该行
                ++x;

        }
        return false;
    }

    // 253.会议室II

    // 283.移动零
    // 给定一个数组 nums，编写一个函数将所有 0 移动到数组的末尾，同时保持非零元素的相对顺序。
    // 说明:
    // 必须在原数组上操作，不能拷贝额外的数组。
    // 尽量减少操作次数

    // 方法一：双指针（基于交换）-时间复杂度：O(n)，空间复杂度：O(1)
    // 左指针指向当前已经处理好的序列的尾部，右指针指向待处理序列的头部。
    public void moveZeroes(int[] nums) {
        int n = nums.length;
        int left = 0;// 期望指向0
        int right = 0;// 交换时指向数字
        while (right < n) {
            if (nums[right] != 0) {
                swap(nums, left, right);
                left++;
                right++;
            } else
                right++;
        }
    }
    // public void swap(int[] nums, int i, int j) {
    // int temp = nums[i];
    // nums[i] = nums[j];
    // nums[j] = temp;
    // }

    // 方法二：自己写的lowb找规律（基于移动）-时间复杂度：O(n)，空间复杂度：O(1)
    public void moveZeroes2(int[] nums) {
        int n = nums.length;
        int zeroCount = 0;
        // 每次移动非0元素即达到其最终位置
        for (int i = 0; i < n; i++) {
            if (nums[i] == 0)
                zeroCount += 1;
            else
                nums[i - zeroCount] = nums[i];
        }
        // 末尾置0
        for (int j = n - zeroCount; j < n; j++)
            nums[j] = 0;
    }

    // 287.寻找重复数

    // 300.最长递增子序列
    // 给你一个整数数组 nums ，找到其中最长严格递增子序列的长度。
    // 子序列是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。
    // 例如，[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
    // 提示：
    // 1 <= nums.length <= 2500
    // -104 <= nums[i] <= 104
    // 进阶：
    // 你可以设计时间复杂度为 O(n2) 的解决方案吗？
    // 你能将算法的时间复杂度降低到 O(n log(n)) 吗?

    // 方法一：动态规划-时间复杂度：O(n^2)，空间复杂度：O(n)
    // dp数组：索引为第 i 个数字做结尾，值为第 i 个数字结尾的最长上升子序列的长度
    // 值不具有单调性，只能挨个遍历，找到结尾小于当前数时，最大的个数
    // 定义 dp[i] 为考虑前 i 个元素，以第 i 个数字结尾的最长上升子序列的长度
    // 状态转移方程为：
    // dp[i] = max(dp[j]) + 1,其中0 ≤ j < i且 num[j] < num[i]
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0)
            return 0;

        int[] dp = new int[nums.length];
        dp[0] = 1;
        int maxans = 1;
        for (int i = 1; i < nums.length; i++) {// 以第i个数字为结尾
            dp[i] = 1; // 如果i小于前面所有数，则dp[i]为1
            for (int j = 0; j < i; j++)// 从头开始
                if (nums[j] < nums[i])
                    dp[i] = Math.max(dp[i], dp[j] + 1);// 长度+1

            maxans = Math.max(maxans, dp[i]);
        }
        return maxans;
    }

    // 方法二：贪心 + 二分查找-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // dp数组：索引为以数字结尾的最长上升子序列的长度，值为该长度下的最小值
    // 此时值必单调，可以使用二分查找加快查找速度，找到合适的序列进行追加
    // 考虑一个简单的贪心，如果我们要使上升子序列尽可能的长，则我们需要让序列上升得尽可能慢，
    // 因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
    // 维护一个数组 d[i] ，表示长度为 i 的最长上升子序列的末尾元素的最小值，且数组d具有单调性（有单调性才能二分）
    public int lengthOfLIS2(int[] nums) {
        int length = 1;// length 记录目前最长上升子序列的长度
        int n = nums.length;

        int[] d = new int[n + 1];// 长度为 i 的最长上升子序列的末尾元素的最小值
        d[length] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > d[length])// 当前值可使长度加1，更新d数组（其实无需单独判断这一步）
                d[++length] = nums[i];
            else {// 当前值无法使长度加1，更新d数组（一定能更新，值一样就覆盖）
                int l = 1, r = length;
                // 找到第一个比 nums[i] 小的数 d[k]
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (d[mid] < nums[i])
                        l = mid + 1;
                    else
                        r = mid - 1;
                }
                d[l] = nums[i];
            }
        }
        return length;
    }

    // 方法二：贪心 + 二分查找（javaAPI）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public int lengthOfLIS22(int[] nums) {
        int n = nums.length;// length 记录目前最长上升子序列的长度
        int dp[] = new int[n + 1];// 长度为 i 的最长上升子序列的末尾元素的最小值
        int length = 1;
        dp[length] = nums[0];
        for (int i = 1; i < n; i++) {
            int numi = nums[i];
            int numj = dp[length];
            if (numi > numj) {// 当前值可使长度加1，更新d数组（其实无需单独判断这一步）
                length++;
                dp[length] = numi;
            } else {// 当前值无法使长度加1，更新d数组（index为负表示插入位置，更新；为正表示找到相同值，不用更新）
                int index = Arrays.binarySearch(dp, 1, length + 1, numi);
                if (index < 0) {
                    index = -index - 1;
                    dp[index] = numi;
                }
            }
        }
        return length;
    }

    // 方法二：（自己写的）贪心 + 二分查找（javaAPI）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public int lengthOfLIS222(int[] nums) {
        int res = 1;
        int n = nums.length;

        int[] dp = new int[n + 1]; // 索引为长度，值为结尾最小元素
        // 预处理，方便后续二分查找
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = Integer.MIN_VALUE;

        for (int i = 0; i < n; i++) {
            int num = nums[i];
            int index = Arrays.binarySearch(dp, 0, res + 1, num);
            // index为正时，存在相同值为结尾的序列，不用更新
            // index为负时，转化为应当插入的位置，即以该值为结尾序列的长度，一定能更新（二分查找递增序列）
            if (index < 0) {
                index = -index - 1;
                dp[index] = num;
                res = Math.max(res, index);
            }
        }
        return res;
    }

    // 309.最佳买卖股票时机含冷冻期
    // 给定一个整数数组，其中第 i 个元素代表了第 i 天的股票价格 。​
    // 设计一个算法计算出最大利润。在满足以下约束条件下，你可以尽可能地完成更多的交易（多次买卖一支股票）:
    // 你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。
    // 卖出股票后，你无法在第二天买入股票 (即冷冻期为 1 天)。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(n)
    // 用 f[i] 表示第 i 天结束之后的「累计最大收益」。
    // 会有三种不同的状态：
    // 我们目前持有一支股票，对应的「累计最大收益」记为 f[i][0]；
    // 我们目前不持有任何股票，并且处于冷冻期中，对应的「累计最大收益」记为 f[i][1]；
    // 我们目前不持有任何股票，并且不处于冷冻期中，对应的「累计最大收益」记为 f[i][2]。
    // 这里的「处于冷冻期」指的是在第 i 天结束之后的状态。也就是说：如果第 i 天结束之后处于冷冻期，那么第 i+1 天无法买入股票。
    // f[i][0]=max(f[i−1][0],f[i−1][2]−prices[i])
    // f[i][1]=f[i−1][0]+prices[i]
    // f[i][2]=max(f[i−1][1],f[i−1][2])
    // 如果在最后一天（第 n-1 天）结束之后，手上仍然持有股票，那么显然是没有任何意义的。
    // 因此更加精确地，最终的答案实际上是：max(f[n−1][1],f[n−1][2])
    // 细节：
    // 我们可以将第 0 天的情况作为动态规划中的边界条件：
    // f[0][0] = -prices[0]
    // f[0][1] = 0
    // f[0][2] = 0
    public int maxProfitWithChill(int[] prices) {
        if (prices.length == 0)
            return 0;

        // 我们目前持有一支股票，对应的「累计最大收益」记为 f[i][0]；
        // 我们目前不持有任何股票，并且处于冷冻期中，对应的「累计最大收益」记为 f[i][1]；（今天卖了，明天不可买）
        // 我们目前不持有任何股票，并且不处于冷冻期中，对应的「累计最大收益」记为 f[i][2]。（明天可买）
        int n = prices.length;
        int f0 = -prices[0];
        int f1 = 0;
        int f2 = 0;

        for (int i = 1; i < n; ++i) {
            int newf0 = Math.max(f0, f2 - prices[i]);// 1.昨天及之前买入的，2.或今天买入的
            int newf1 = f0 + prices[i];// 今天卖出
            int newf2 = Math.max(f1, f2);// 昨天及之前卖出。1.昨天卖出 2.昨天之前卖出

            f0 = newf0;
            f1 = newf1;
            f2 = newf2;
        }

        return Math.max(f1, f2);
    }

    // 312.戳气球
    // 有 n 个气球，编号为0 到 n - 1，每个气球上都标有一个数字，这些数字存在数组 nums 中。
    // 现在要求你戳破所有的气球。戳破第 i 个气球，你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 
    // 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。
    // 如果 i - 1或 i + 1 超出了数组的边界，那么就当它是一个数字为 1 的气球。
    // 求所能获得硬币的最大数量。
    // 提示：
    // n == nums.length
    // 1 <= n <= 500
    // 0 <= nums[i] <= 100

    // 方法一：记忆化搜索（分治）-时间复杂度：O(n^3)，空间复杂度：O(n^2)
    // 我们观察戳气球的操作，发现这会导致两个气球从不相邻变成相邻，使得后续操作难以处理。
    // 于是我们倒过来看这些操作，将全过程看作是每次添加一个气球。

    // 为了防止重复计算，我们存储 solve 的结果，使用记忆化搜索的方法优化时间复杂度。
    public int[][] rec;

    // 对 nums 数组稍作处理，将其两边各加上题目中假设存在的 nums[−1] 和 nums[n] ，并保存在val 数组中，
    // 即 val[i]=nums[i−1] 。
    // 之所以这样处理是为了处理 nums[−1] ，防止下标越界。
    public int[] val;

    public int maxCoins(int[] nums) {
        int n = nums.length;
        val = new int[n + 2];
        for (int i = 1; i <= n; i++)
            val[i] = nums[i - 1];
        val[0] = val[n + 1] = 1;

        rec = new int[n + 2][n + 2];// rec[i][j]表示开区间 (i,j) 内的位置全部填满气球能够得到的最多硬币数。
        for (int i = 0; i <= n + 1; i++) // 初始置为-1
            Arrays.fill(rec[i], -1);

        return solve(0, n + 1);
    }

    // solve(i,j) 表示将开区间 (i,j) 内的位置全部填满气球能够得到的最多硬币数。
    public int solve(int left, int right) {
        if (left >= right - 1) // 开区间(left, right)需满足 r > l + 1
            return 0;

        if (rec[left][right] != -1) // 已经计算过
            return rec[left][right];

        // 求解开区间(left, right)
        for (int i = left + 1; i < right; i++) {
            int sum = val[left] * val[i] * val[right];// 先放i，即最后戳i
            sum += solve(left, i) + solve(i, right);// i后续放的气球分别在左在右，其相对顺序不影响 (left, i) (i, right)
            rec[left][right] = Math.max(rec[left][right], sum);// 更新rec数组
        }
        return rec[left][right];
    }

    // 方法二：动态规划-时间复杂度：O(n^3)，空间复杂度：O(n^2)
    // 以i, j为边界 依次插入 i+1, ...,j-1 元素
    // 通过变换计算顺序，从「自顶向下」的记忆化搜索变为「自底向上」的动态规划。
    public int maxCoins2(int[] nums) {
        int n = nums.length;

        int[][] rec = new int[n + 2][n + 2];// 默认初始值为0

        // 防越界处理
        int[] val = new int[n + 2];
        val[0] = val[n + 1] = 1;
        for (int i = 1; i <= n; i++)
            val[i] = nums[i - 1];

        for (int i = n - 1; i >= 0; i--)
            for (int j = i + 2; j <= n + 1; j++)
                // 求解开区间(i, j)，i j 之间最小长度为3
                // 当i k , j k 相差为1时，rec为0（初始化默认值），此时i j 相差为2，rec[i][j]仅为sum = val[i] * val[k] *
                // val[j]
                for (int k = i + 1; k < j; k++) {
                    int sum = val[i] * val[k] * val[j];// 该区间内第一个插入的值
                    sum += rec[i][k] + rec[k][j];// 分别插入两边填满该区间
                    rec[i][j] = Math.max(rec[i][j], sum);
                }

        return rec[0][n + 1];
    }

    // 方法二：（自己写的）动态规划-时间复杂度：O(n^3)，空间复杂度：O(n^2)
    // 以i-1, j+1为边界 依次插入 i, ...,j 元素（个人认为更符合代码习惯）
    public int maxCoins22(int[] nums) {
        int n = nums.length;

        // 复制nums数组，放入边界，方便后续计算
        // 0 [1 ... n] n+1
        int[] dummyNums = new int[n + 2];
        dummyNums[0] = 1;
        dummyNums[n + 1] = 1;
        for (int i = 1; i <= n; i++)
            dummyNums[i] = nums[i - 1];

        int[][] dp = new int[n + 2][n + 2];

        for (int right = 1; right <= n; right++) {
            for (int left = right; left >= 1; left--) {
                int ans = 0;
                for (int index = left; index <= right; index++) {
                    // 右边界小于左边界时，返回数组默认值0
                    int dpLeft = dp[left][index - 1];
                    int dpRight = dp[index + 1][right];

                    int num = dummyNums[index];// 第一个插入的元素，即最后一个戳爆的元素

                    // 插入num后，左边、右边插满后的最大值
                    int leftBound = dummyNums[left - 1];
                    int rightBound = dummyNums[right + 1];

                    ans = Math.max(ans, leftBound * num * rightBound + dpLeft + dpRight);
                }
                dp[left][right] = ans;
            }
        }
        return dp[1][n];
    }

    // 322.零钱兑换

    // 347.前K个高频元素（哈希表计数后，问题转化为「215. 数组中的第K个最大元素」）
    // 给你一个整数数组 nums 和一个整数 k ，请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
    // 提示：
    // 1 <= nums.length <= 105
    // k 的取值范围是 [1, 数组中不相同的元素的个数]
    // 题目数据保证答案唯一，换句话说，数组中前 k 个高频元素的集合是唯一的
    // 进阶：你所设计算法的时间复杂度 必须 优于 O(n log n) ，其中 n 是数组大小。

    // 方法一：哈希表+堆-时间复杂度：O(Nlogk)，空间复杂度：O(N)
    // 首先遍历整个数组，并使用哈希表记录每个数字出现的次数，并形成一个「出现次数数组」。
    // 找出原数组的前 k 个高频元素，就相当于找出「出现次数数组」的前 k 大的值。
    // 最简单的做法是给「出现次数数组」排序。但由于可能有 O(N) 个不同的出现次数（其中 N 为原数组长度），
    // 故总的算法复杂度会达到 O(NlogN)，不满足题目的要求。

    // 选取k个最大值的另一种思路：（不是数组映射到根数，而是直接使用优先队列）
    // 建立一个小顶堆，然后遍历「出现次数数组」：
    // 如果堆的元素个数小于 k，就可以直接插入堆中。
    // 如果堆的元素个数等于 k，则检查堆顶与当前出现次数的大小。
    // 如果堆顶更大，说明至少有 k 个数字的出现次数比当前值大，故舍弃当前值；否则，就弹出堆顶，并将当前值插入堆中。
    // 遍历完成后，堆中的元素就代表了「出现次数数组」中前 k 大的值。
    public int[] topKFrequent(int[] nums, int k) {
        // 哈希表记录每个数字出现的次数，并形成一个「出现次数数组」。
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums)
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);

        // int[] 的第一个元素代表数组的值，第二个元素代表了该值出现的次数
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>((m, n) -> m[1] - n[1]);

        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            if (queue.size() == k) {
                if (queue.peek()[1] < count) {// 弃掉堆顶和当前值中更小的
                    queue.poll();
                    queue.offer(new int[] { num, count });
                }

            } else// 优先队列中不足k个则直接入队
                queue.offer(new int[] { num, count });

        }

        int[] ret = new int[k];
        for (int i = 0; i < k; ++i)
            ret[i] = queue.poll()[0];

        return ret;

    }

    // 方法二：哈希表+基于快速排序-时间复杂度：O(N^2)（最坏），空间复杂度：O(N)
    // 使用基于快速排序的方法，求出「出现次数数组」的前 k 大的值。
    public int[] topKFrequent2(int[] nums, int k) {
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums)
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);

        // entrySet转List
        List<int[]> values = new ArrayList<int[]>();
        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            values.add(new int[] { num, count });
        }

        int[] ret = new int[k];
        qsort(values, 0, values.size() - 1, ret, 0, k);
        return ret;
    }

    // 选边快排（降序，递归形式）
    public void qsort(List<int[]> values, int start, int end, int[] ret, int retIndex, int k) {
        // 还是随机选择pivot
        int picked = (int) (Math.random() * (end - start + 1)) + start;
        int pivot = values.get(picked)[1];
        Collections.swap(values, picked, start);

        // 快排，确定pivot的最终位置，即index
        int index = start;
        for (int i = start + 1; i <= end; i++)
            if (values.get(i)[1] >= pivot) {
                Collections.swap(values, index + 1, i);
                index++;
            }

        Collections.swap(values, start, index);

        if (k <= index - start) {// pivot最终位置不在前k以内，则不是要找的答案，继续对左半边进行递归
            qsort(values, start, index - 1, ret, retIndex, k);
        } else {// pivot最终位置在前k以内
            for (int i = start; i <= index; i++) // index之前的放入答案ret
                ret[retIndex++] = values.get(i)[0];

            if (k > index - start + 1) // k = index - start时，全部答案已找到
                // 问题转化为查找右半边的前 k - (index - start + 1) 个最大值
                qsort(values, index + 1, end, ret, retIndex, k - (index - start + 1));

        }
    }

    // 399.除法求值
    // 406.根据身高重建队列

    // 416.分割等和子集
    // 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。
    // 提示：
    // 1 <= nums.length <= 200
    // 1 <= nums[i] <= 100
    // 本题是经典的「NP 完全问题」

    // 方法一：动态规划-时间复杂度：O(n×target)，其中 target 是整个数组的元素和的一半；空间复杂度：O(target)
    // 精髓，操作dp数组时，一定要注意遍历顺序
    public boolean canPartition(int[] nums) {
        // 排除特殊情况
        int n = nums.length;
        if (n < 2)
            return false;

        int sum = 0, maxNum = 0;
        for (int num : nums) {
            sum += num;
            maxNum = Math.max(maxNum, num);
        }

        if (sum % 2 != 0)
            return false;

        int target = sum / 2;
        if (maxNum > target)
            return false;

        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            for (int j = target; j >= num; --j) // dp[j]= true当且仅当j可达（存在dp值加上当前元素 = j）
                dp[j] |= dp[j - num];

        }
        return dp[target];
    }

    // 448.找到所有数组中消失的数字
    // 给你一个含 n 个整数的数组 nums ，其中 nums[i] 在区间 [1, n] 内。
    // 请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字，并以数组的形式返回结果。
    // 提示：
    // n == nums.length
    // 1 <= n <= 105
    // 1 <= nums[i] <= n
    // 进阶：你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。

    // 方法一：原地修改-时间复杂度：O(n)，空间复杂度：O(1)
    // 我们可以用一个哈希表记录数组 nums 中的数字，由于数字范围均在 [1,n] 中，
    // 记录数字后我们再利用哈希表检查 [1,n] 中的每一个数是否出现，从而找到缺失的数字。
    // 由于数字范围均在 [1,n]中，我们也可以用一个长度为 n 的数组来代替哈希表。
    // 这一做法的空间复杂度是 O(n) 的。
    // 我们的目标是优化空间复杂度到 O(1)。

    // 注意到 nums 的长度恰好也为 nn，能否让 nums 充当哈希表呢？
    // 由于 nums 的数字范围均在 [1,n] 中，我们可以利用这一范围之外的数字，来表达「是否存在」的含义。
    public List<Integer> findDisappearedNumbers(int[] nums) {
        int n = nums.length;
        // 遍历 nums，每遇到一个数 x，就让 nums[x−1] 增加 n。
        // 由于 nums 中所有数均在 [1,n] 中，增加以后，这些数必然大于 n。
        // 注意，当我们遍历到某个位置时，其中的数可能已经被增加过，因此需要对 n 取模来还原出它本来的值。
        for (int num : nums) {
            int x = (num - 1) % n;
            nums[x] += n;
        }

        List<Integer> ret = new ArrayList<Integer>();
        // 最后我们遍历nums，若 nums[i] 未大于 n，就说明没有遇到过数 i+1。这样我们就找到了缺失的数字。
        for (int i = 0; i < n; i++)
            if (nums[i] <= n)
                ret.add(i + 1);

        return ret;
    }

    // 494.目标和
    // 给你一个整数数组 nums 和一个整数 target 。
    // 向数组中的每个整数前添加 '+' 或 '-' ，然后串联起所有整数，可以构造一个 表达式 ：
    // 例如，nums = [2, 1] ，可以在 2 之前添加 '+' ，在 1 之前添加 '-' ，然后串联起来得到表达式 "+2-1" 。
    // 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
    // 提示：
    // 1 <= nums.length <= 20
    // 0 <= nums[i] <= 1000
    // 0 <= sum(nums[i]) <= 1000
    // -1000 <= target <= 1000

    // 方法一：回溯-时间复杂度：O(2^n)，空间复杂度：O(n)
    int count = 0;

    public int findTargetSumWays(int[] nums, int target) {
        backtrack(nums, target, 0, 0);
        return count;
    }

    public void backtrack(int[] nums, int target, int index, int sum) {
        if (index == nums.length) {
            if (sum == target)
                count++;

        } else {
            backtrack(nums, target, index + 1, sum + nums[index]);
            backtrack(nums, target, index + 1, sum - nums[index]);
        }
    }

    // 方法二：动态规划-时间复杂度：O(n×(sum−target))，空间复杂度：O(sum−target)
    // 记数组的元素和为 um，添加 - 号的元素之和为 neg，则其余添加 + 的元素之和为 sum−neg，
    // 得到的表达式的结果为 (sum−neg) − neg = sum − 2⋅neg = target
    // neg = (sum−target)/2
    // 由于数组 nums 中的元素都是非负整数，neg 也必须是非负整数，所以上式成立的前提是 sum−target 是非负偶数。
    // 若不符合该条件可直接返回 0。

    // 此时问题转化成在数组 nums 中选取若干元素，使得这些元素之和等于 neg，计算选取元素的方案数。
    // 这是典型的背包问题。我们可以使用动态规划的方法求解。
    public int findTargetSumWays2(int[] nums, int target) {
        int sum = 0;
        for (int num : nums)
            sum += num;

        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0)
            return 0;

        // 定义数组 dp，其中 dp[i][j] 表示在数组 nums 的前 i 个数中选取元素，使得这些元素之和等于 j 的方案数。
        // 假设数组 nums 的长度为 n，则最终答案为dp[n][neg]。
        int n = nums.length, neg = diff / 2;// 问题转化后的目标和
        int[][] dp = new int[n + 1][neg + 1];// 默认初始化为0
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {// 第i个数
            int num = nums[i - 1];
            for (int j = 0; j <= neg; j++) {// 和为j
                dp[i][j] = dp[i - 1][j];// 继承上一状态
                if (num <= j)// 第i个数小于j，可以放入背包，否则不放入
                    dp[i][j] += dp[i - 1][j - num];

            }
        }
        return dp[n][neg];
    }

    // 方法三：动态规划 + 空间优化-时间复杂度：O(n×(sum−target))，空间复杂度：O(sum−target)
    public int findTargetSumWays3(int[] nums, int target) {
        int sum = 0;
        for (int num : nums)
            sum += num;

        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0)
            return 0;

        int neg = diff / 2;// 问题转化后的目标和
        int[] dp = new int[neg + 1];// 默认初始化为0
        dp[0] = 1;
        for (int num : nums)// 第i个数
            for (int j = neg; j >= num; j--) // 注意顺序，从后往前（第i个数小于j才放入背包）
                dp[j] += dp[j - num];

        return dp[neg];
    }

    // 方法三：自己写的dp
    // pos + neg = sum
    // pos - neg = target
    // pos = (target + sum) / 2
    public int findTargetSumWays33(int[] nums, int target) {
        int sum = Arrays.stream(nums).sum();
        if (target > sum || target < -sum)
            return 0;

        if ((target + sum) % 2 == 1)
            return 0;
        int pos = (target + sum) / 2;
        int dp[] = new int[pos + 1];
        dp[0] = 1;
        for (int num : nums)
            for (int j = pos; j >= num; j--)
                dp[j] = dp[j] + dp[j - num];
        return dp[pos];
    }

    // 560.和为K的子数组
    // 给你一个整数数组 nums 和一个整数 k ，请你统计并返回该数组中和为 k 的连续子数组的个数。
    // 提示：
    // 1 <= nums.length <= 2 * 104
    // -1000 <= nums[i] <= 1000
    // -107 <= k <= 107（注意k可以为负，所以不能使用滑动窗口或者双指针，只能使用前缀和）

    // 方法一：枚举-时间复杂度：O(n^2)，空间复杂度：O(1)

    // 方法二：前缀和 + 哈希表优化-时间复杂度：O(n)，空间复杂度：O(n)
    // 前缀和作差的思想
    // 精髓：计算前缀和的同时，计算结果
    public int subarraySum(int[] nums, int k) {
        int count = 0;
        int pre = 0;// pre为 [0..i] 里所有数的和
        HashMap<Integer, Integer> mp = new HashMap<>();// 以前缀和pre为键，出现次数为对应的值
        mp.put(0, 1);// 该键值对的作用是，pre直接等于k时，能使count++
        for (int i = 0; i < nums.length; i++) {
            pre += nums[i];
            if (mp.containsKey(pre - k)) // 当前前缀和pre与之前前缀和作差，能得到所有以当前元素结尾的子数组
                count += mp.get(pre - k);

            mp.put(pre, mp.getOrDefault(pre, 0) + 1);// 注意顺序，先计算结果，再把前缀和放入哈希表
        }
        return count;
    }

    // 581.最短无序连续子数组
    // 621.任务调度器
    // 739.每日温度

}

// 哈希表
class HashTableSol {
    // 1.两数之和

    // 3.无重复字符的最长子串
    // 给定一个字符串 s ，请你找出其中不含有重复字符的 最长子串 的长度。
    // 提示：
    // 0 <= s.length <= 5 * 104
    // s 由英文字母、数字、符号和空格组成

    // 方法一：滑动窗口-时间复杂度：O(N)，空间复杂度：O(∣Σ∣)，∣Σ∣ 表示字符集的大小。
    // 优化：使用HashMap一并存储所在索引，左指针直接跳到索引处
    public int lengthOfLongestSubstring(String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        int max = 0, start = 0;
        for (int end = 0; end < s.length(); end++) {
            char ch = s.charAt(end);
            if (map.containsKey(ch))
                start = Math.max(map.get(ch) + 1, start);// 精髓所在，存在于map，但是不在[left, right]范围内，直接更新map就好

            max = Math.max(max, end - start + 1);
            map.put(ch, end);
        }
        return max;
    }

    // 17.电话号码的字母组合（其实用数组也可以）
    // 给定一个仅包含数字 2-9 的字符串，返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
    // 给出数字到字母的映射如下（与电话按键相同）。注意 1 不对应任何字母。
    // 提示：
    // 0 <= digits.length <= 4
    // digits[i] 是范围 ['2', '9'] 的一个数字。

    // 方法一：dfs 回溯（不存在不可行的解，直接穷举即可）-时间复杂度：O(3^m × 4^n)，空间复杂度：O(m+n)
    public List<String> letterCombinations(String digits) {
        List<String> combinations = new ArrayList<String>();
        if (digits.length() == 0)
            return combinations;

        Map<Character, String> phoneMap = new HashMap<Character, String>() {
            {
                put('2', "abc");
                put('3', "def");
                put('4', "ghi");
                put('5', "jkl");
                put('6', "mno");
                put('7', "pqrs");
                put('8', "tuv");
                put('9', "wxyz");
            }
        };
        backtrack(combinations, phoneMap, digits, 0, new StringBuffer());
        return combinations;
    }

    public void backtrack(List<String> combinations, Map<Character, String> phoneMap, String digits, int index,
            StringBuffer combination) {
        if (index == digits.length()) // 到头了，加入答案List
            combinations.add(combination.toString());
        else {
            char digit = digits.charAt(index);
            String letters = phoneMap.get(digit);
            int lettersCount = letters.length();
            for (int i = 0; i < lettersCount; i++) {
                combination.append(letters.charAt(i));
                backtrack(combinations, phoneMap, digits, index + 1, combination);
                combination.deleteCharAt(index);// 递归完后，删除当前字符
            }
        }
    }

    // 方法二：bfs队列辅助

    // 49.字母异位词分组
    // 给你一个字符串数组，请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
    // 字母异位词 是由重新排列源单词的字母得到的一个新单词，所有源单词中的字母都恰好只用一次。
    // 提示：
    // 1 <= strs.length <= 104
    // 0 <= strs[i].length <= 100
    // strs[i] 仅包含小写字母

    // 本题关键在于用什么作键（键尽可能短）
    // 引用数据类型当键，必须是地址相同才行（equal()逻辑），String除外

    // 方法一：排序（排序之后的字符串作为哈希表的键）-时间复杂度：O(nklogk)，空间复杂度：O(nk)
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        for (String str : strs) {
            char[] array = str.toCharArray();
            Arrays.sort(array);
            String key = new String(array);
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<>(map.values());
    }

    // 方法二：计数（出现次数大于 0 的字母和出现次数按顺序拼接成字符串，作为哈希表的键）
    // 时间复杂度：O(n(k+∣Σ∣))，空间复杂度：O(n(k+∣Σ∣))
    public List<List<String>> groupAnagrams2(String[] strs) {
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        for (String str : strs) {
            int[] counts = new int[26];
            int length = str.length();
            for (int i = 0; i < length; i++)
                counts[str.charAt(i) - 'a']++;

            // 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串，作为哈希表的键
            // 不能只用出现次数拼接， 0 10， 0 1 0 无法区分
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 26; i++)
                if (counts[i] != 0) {// 精髓所在：只拼接非0次数，尽可能减少字符键的长度！
                    sb.append((char) ('a' + i));
                    sb.append(counts[i]);
                }

            String key = sb.toString();// 精髓所在：String重写了equals，比较的是内容，StringBuilder没有重写equals，比较的是地址
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<List<String>>(map.values());
    }

    // 方法三：自己的lowb想法 并查集（字母异位词可找到共键，直接哈希表.values()转List得到答案）

    // 76.最小覆盖子串
    // 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串，则返回空字符串 "" 。
    // 注意：
    // 对于 t 中重复字符，我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
    // 如果 s 中存在这样的子串，我们保证它是唯一的答案。
    // 提示：
    // 1 <= s.length, t.length <= 105
    // s 和 t 由英文字母组成
    // 进阶：你能设计一个在 o(n) 时间内解决此问题的算法吗？

    // 方法一：滑动窗口-时间复杂度：O(C⋅∣s∣+∣t∣)，字符集大小为 C，空间复杂度：O(C)
    // 我们在 s 上滑动窗口，通过移动 r 指针不断扩张窗口。当窗口包含 t 全部所需的字符后，
    // 如果能收缩，我们就收缩窗口直到得到最小窗口。
    Map<Character, Integer> ori = new HashMap<Character, Integer>();// 字符串t的哈希表
    Map<Character, Integer> cnt = new HashMap<Character, Integer>();// （当前窗口内）字符计数的哈希表

    public String minWindow(String s, String t) {
        // 填充ori
        for (int i = 0; i < t.length(); i++) {
            char c = t.charAt(i);
            ori.put(c, ori.getOrDefault(c, 0) + 1);
        }

        int l = 0, r = -1;
        int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;// 窗口大小，窗口左右边界
        int sLen = s.length();
        while (r < sLen) {
            ++r; // 一次移动一步
            if (r < sLen && ori.containsKey(s.charAt(r)))// 当前字符为目标t中的字符，加入计数哈希表
                cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);

            while (check()) {// 当前窗口涵盖 t 所有字符的子串
                // 窗口大小变小，更新窗口边界
                if (r - l + 1 < len) {
                    len = r - l + 1;
                    ansL = l;
                    ansR = l + len;
                }
                // 准备移动窗口左边界，更新计数哈希表
                if (ori.containsKey(s.charAt(l)))
                    cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
                ++l;// 一次移动一步
            }
        }
        return ansL == -1 ? "" : s.substring(ansL, ansR);
    }

    // 检查当前窗口是否涵盖 t 所有字符的子串
    public boolean check() {
        Iterator<Map.Entry<Character, Integer>> iter = ori.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<Character, Integer> entry = iter.next();
            Character key = entry.getKey();
            Integer val = entry.getValue();
            if (cnt.getOrDefault(key, 0) < val)
                return false;

        }
        return true;
    }

    // 方法二：优化方法一，预处理 s，扔掉那些 t 中没有出现的字符，然后再做滑动窗口呢？
    // 或者在移动窗口时，一次跳多步

    // 方法二：自己写的（每次移动左，循环移动右，还可以每次移动右，循环移动左）
    public String minWindow2(String s, String t) {
        int n = s.length();
        Map<Character, Integer> strt = new HashMap<>();// 目标map
        Map<Character, Integer> window = new HashMap<>();// 窗口map
        for (char c : t.toCharArray())
            strt.put(c, strt.getOrDefault(c, 0) + 1);

        // 初始化窗口
        int right = -1;
        while (!check(strt, window)) {
            right++;
            if (right >= n)
                return "";
            char c = s.charAt(right);
            window.put(c, window.getOrDefault(c, 0) + 1);
        }
        int minLength = right + 1;
        int minLeft = 0;
        int minRight = right;

        // 每次移动左边界
        for (int left = 1; left < n; left++) {
            char removeChar = s.charAt(left - 1);
            if (strt.containsKey(removeChar)) {// 出窗元素在目标map里
                window.put(removeChar, window.get(removeChar) - 1);
                if (window.get(removeChar) < strt.get(removeChar)) {// 更新window后，不满足目标map
                    while (right < n) {// 移动右边界，直到遇到出窗元素
                        right++;
                        if (right >= n)
                            break;
                        char addChar = s.charAt(right);
                        window.put(addChar, window.getOrDefault(addChar, 0) + 1);
                        if (addChar == removeChar)
                            break;
                    }
                    if (right >= n)// 右边界越界
                        break;
                }
            }
            // 更新minLenth
            if (right - left + 1 < minLength) {
                minLength = right - left + 1;
                minLeft = left;
                minRight = right;
            }

        }

        return s.substring(minLeft, minRight + 1);
    }

    private boolean check(Map<Character, Integer> strt, Map<Character, Integer> window) {
        Set<Character> strtSet = strt.keySet();
        for (Character c : strtSet)
            if (window.getOrDefault(c, 0) < strt.get(c))
                return false;
        return true;
    }

    // 方法二：自己写的（每次移动右，循环移动左，调用比较函数更多，更推荐！）
    public String minWindow22(String s, String t) {
        Map<Character, Integer> sLetterCount = new HashMap<>();
        Map<Character, Integer> tLetterCount = new HashMap<>();
        int left = 0;
        int right = 0;
        int leftRes = 0;
        int rightRes = Integer.MAX_VALUE - 1;

        for (char c : t.toCharArray()) {
            tLetterCount.put(c, tLetterCount.getOrDefault(c, 0) + 1);
        }

        for (right = 0; right < s.length(); right++) {
            char cur = s.charAt(right);
            if (tLetterCount.containsKey(cur)) {
                sLetterCount.put(cur, sLetterCount.getOrDefault(cur, 0) + 1);
                if (contains(sLetterCount, tLetterCount)) {
                    while (true) {
                        char leftChar = s.charAt(left);
                        if (sLetterCount.containsKey(leftChar)) {
                            if (sLetterCount.get(leftChar) > tLetterCount.get(leftChar))
                                sLetterCount.put(leftChar, sLetterCount.get(leftChar) - 1);
                            else
                                break;
                        }
                        left++;
                    }

                    if (right - left + 1 < rightRes - leftRes + 1) {
                        leftRes = left;
                        rightRes = right;
                    }
                }
            }
        }
        if (rightRes == Integer.MAX_VALUE - 1)
            return "";
        else
            return s.substring(leftRes, rightRes + 1);
    }

    public boolean contains(Map<Character, Integer> sLetterCount, Map<Character, Integer> tLetterCount) {
        for (char c : tLetterCount.keySet()) {
            if (sLetterCount.getOrDefault(c, 0) < tLetterCount.get(c))
                return false;
        }
        return true;
    }

    // 方法三：（自己写的）作差优化，不使用比较函数
    public String minWindow3(String s, String t) {
        Map<Character, Integer> charCount = new HashMap<>();
        int sLen = s.length();
        int tLen = t.length();

        if (sLen < tLen)
            return "";

        for (int i = 0; i < tLen; i++) {
            char c = t.charAt(i);
            charCount.put(c, charCount.getOrDefault(c, 0) - 1);
        }

        int NegCount = charCount.size();

        int left = 0;
        int right = 0;
        int resLeft = 0;
        int resRight = Integer.MAX_VALUE;

        for (right = 0; right < sLen; right++) {
            char c = s.charAt(right);
            if (charCount.containsKey(c)) {
                charCount.put(c, charCount.get(c) + 1);
                if (charCount.get(c) == 0)
                    NegCount--;

                if (NegCount == 0) {
                    while (left < right) {
                        char remove = s.charAt(left);
                        if (charCount.containsKey(remove)) {
                            if (charCount.get(remove) == 0)
                                break;
                            charCount.put(remove, charCount.get(remove) - 1);
                        }
                        left++;
                    }
                    if (right - left < resRight - resLeft) {
                        resRight = right;
                        resLeft = left;
                    }
                }
            }
        }
        return resRight - resLeft == Integer.MAX_VALUE ? "" : s.substring(resLeft, resRight + 1);
    }

    // 105.从前序与中序遍历序列构造二叉树
    // 128.最长连续序列
    // 139.单词拆分

    // 141.环形链表
    // 给你一个链表的头节点 head ，判断链表中是否有环。
    // 如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。
    // 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。
    // 如果 pos 是 -1，则在该链表中没有环。注意：pos 不作为参数进行传递，仅仅是为了标识链表的实际情况。
    // 如果链表中存在环，则返回 true 。 否则，返回 false 。
    // 提示：
    // 链表中节点的数目范围是 [0, 104]
    // -105 <= Node.val <= 105
    // pos 为 -1 或者链表中的一个 有效索引 。
    // 进阶：你能用 O(1)（即，常量）内存解决此问题吗？
    class ListNode {
        int val;
        ListNode next;

        ListNode(int x) {
            val = x;
            next = null;
        }
    }

    // 方法一：哈希表-时间复杂度：O(N)，空间复杂度：O(N)
    public boolean hasCycle(ListNode head) {
        Set<ListNode> seen = new HashSet<ListNode>();
        while (head != null) {
            if (!seen.add(head)) // 添加失败
                return true;

            head = head.next;
        }
        return false;
    }

    // 方法二：快慢指针-时间复杂度：O(N)，空间复杂度：O(1)
    // 「Floyd 判圈算法」（又称龟兔赛跑算法）
    // 我们定义两个指针，一快一满。慢指针每次只移动一步，而快指针每次移动两步。
    // 初始时，慢指针在位置 head，而快指针在位置 head.next。
    // 这样一来，如果在移动的过程中，快指针反过来追上慢指针，就说明该链表为环形链表。
    // 否则快指针将到达链表尾部，该链表不为环形链表。
    public boolean hasCycle2(ListNode head) {
        if (head == null || head.next == null) // 头节点为空，或头节点指向null
            return false;

        ListNode slow = head;
        ListNode fast = head.next;
        while (slow != fast) {
            if (fast == null || fast.next == null) // 快指针到达链表尾部
                return false;

            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }

    // 方法三：自己的lowb想法：修改原链表结构，不断修改指向头节点方向，若回到原节点，则含环

    // 142.环形链表II
    // 给定一个链表，返回链表开始入环的第一个节点。 如果链表无环，则返回 null。
    // 如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。
    // 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。
    // 如果 pos 是 -1，则在该链表中没有环。注意：pos 不作为参数进行传递，仅仅是为了标识链表的实际情况。
    // 不允许修改 链表。
    // 提示：
    // 链表中节点的数目范围在范围 [0, 104] 内
    // -105 <= Node.val <= 105
    // pos 的值为 -1 或者链表中的一个有效索引
    // 进阶：你是否可以使用 O(1) 空间解决此题？

    // 方法一：哈希表-时间复杂度：O(N)，空间复杂度：O(N)
    public ListNode detectCycle(ListNode head) {
        Set<ListNode> seen = new HashSet<ListNode>();
        while (head != null) {
            if (!seen.add(head)) // 添加失败
                return head;

            head = head.next;
        }
        return null;
    }

    // 方法二：快慢指针-时间复杂度：O(N)，空间复杂度：O(1)
    // 如果链表中存在环，则 fast 指针最终将再次与 slow 指针在环中相遇。
    // 设链表中环外部分的长度为 a。slow 指针进入环后，又走了 b 的距离与fast 相遇。
    // 此时，fast 指针已经走完了环的 n 圈，因此它走过的总距离为 a+n(b+c)+b=a+(n+1)b+nc。
    // 根据题意，任意时刻，fast 指针走过的距离都为 slow 指针的 2 倍。
    // 因此，我们有
    // a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)
    // 有了 a=c+(n-1)(b+c)a=c+(n−1)(b+c) 的等量关系，我们会发现：
    // 从相遇点到入环点的距离加上 n-1 圈的环长，恰好等于从链表头部到入环点的距离。

    // 因此，当发现 slow 与 fast 相遇时，我们再额外使用一个指针 ptr。起始，它指向链表头部；
    // 随后，它和 slow 每次向后移动一个位置。最终，它们会在入环点相遇。

    public ListNode detectCycle2(ListNode head) {
        if (head == null)
            return null;

        ListNode slow = head, fast = head;
        while (fast != null) {
            if (fast.next == null)
                return null;
            slow = slow.next;
            fast = fast.next.next;
            if (fast == slow) {// 含环，此时开始寻找入环节点
                ListNode ptr = head;
                while (ptr != slow) {
                    ptr = ptr.next;
                    slow = slow.next;
                }
                return ptr;
            }
        }
        return null;
    }

    // 146.LRU缓存机制

    // // 160.相交链表
    // 给你两个单链表的头节点 headA 和 headB ，请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点，返回 null 。
    // 图示两个链表在节点 c1 开始相交：
    // 题目数据 保证 整个链式结构中不存在环。
    // 注意，函数返回结果后，链表必须保持其原始结构 。
    // 自定义评测：
    // 评测系统 的输入如下（你设计的程序 不适用 此输入）：
    // intersectVal - 相交的起始节点的值。如果不存在相交节点，这一值为 0
    // listA - 第一个链表
    // listB - 第二个链表
    // skipA - 在 listA 中（从头节点开始）跳到交叉节点的节点数
    // skipB - 在 listB 中（从头节点开始）跳到交叉节点的节点数
    // 评测系统将根据这些输入创建链式数据结构，并将两个头节点 headA 和 headB 传递给你的程序。
    // 如果程序能够正确返回相交节点，那么你的解决方案将被 视作正确答案 。
    // 提示：
    // listA 中节点数目为 m
    // listB 中节点数目为 n
    // 0 <= m, n <= 3 * 104
    // 1 <= Node.val <= 105
    // 0 <= skipA <= m
    // 0 <= skipB <= n
    // 如果 listA 和 listB 没有交点，intersectVal 为 0
    // 如果 listA 和 listB 有交点，intersectVal == listA[skipA] == listB[skipB]
    // 进阶：你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案？

    // 方法一：哈希集合-时间复杂度：O(m+n)，空间复杂度：O(m)
    // 判断两个链表是否相交，可以使用哈希集合存储链表节点。
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set<ListNode> visited = new HashSet<ListNode>();
        ListNode temp = headA;
        // 首先遍历链表 headA，并将链表 headA 中的每个节点加入哈希集合中。
        while (temp != null) {
            visited.add(temp);
            temp = temp.next;
        }
        temp = headB;
        // 然后遍历链表 headB，对于遍历到的每个节点，判断该节点是否在哈希集合中
        while (temp != null) {
            if (visited.contains(temp))
                return temp;

            temp = temp.next;
        }
        return null;
    }

    // 方法二：双指针-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 精髓在于不相交链表可以看做同时指向null，因此不用做特殊判断
    // 链表 headA 和 headB 的长度分别是 m 和 n。
    // 假设链表 headA 的不相交部分有 a 个节点，链表 headB 的不相交部分有 b 个节点，两个链表相交的部分有 c 个节点，
    // 则有 a+c=m，b+c=n。
    // 如果 a=b，则两个指针会同时到达两个链表相交的节点，此时返回相交的节点；
    // 如果 a!=b，则指针 pA 会遍历完链表 headA，指针 pB 会遍历完链表 headB，两个指针不会同时到达链表的尾节点，
    // 然后指针 pA 移到链表 headB 的头节点，指针 pB 移到链表 headA 的头节点，然后两个指针继续移动，
    // 在指针 pA 移动了 a+c+b 次、指针 pB 移动了b+c+a 次之后，两个指针会同时到达两个链表相交的节点，
    // 该节点也是两个指针第一次同时指向的节点，此时返回相交的节点。
    // 如果两链表不相交，则两指针最后会同时指向null，返回null表示不相交
    public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
        // 一定不相交
        if (headA == null || headB == null)
            return null;

        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }

    // 169.多数元素
    // 208.实现Trie (前缀树)
    // 347.前K个高频元素

    // 438.找到字符串中所有字母异位词
    // 给定两个字符串 s 和 p，找到 s 中所有 p 的 异位词 的子串，返回这些子串的起始索引。不考虑答案输出的顺序。
    // 异位词 指由相同字母重排列形成的字符串（包括相同的字符串）。
    // 提示:
    // 1 <= s.length, p.length <= 3 * 104
    // s 和 p 仅包含小写字母

    // 方法一：滑动窗口 + 数组-时间复杂度：O(mn)，空间复杂度：O(1)
    // 因为字符串中的字符全是小写字母，可以用长度为26的数组记录字母出现的次数
    // 设n = len(s), m = len(p)。记录p字符串的字母频次p_cnt，和s字符串前m个字母频次s_cnt
    // 若p_cnt和s_cnt相等，则找到第一个异位词索引 0
    // 继续遍历s字符串索引为[m, n)的字母，在s_cnt中每次增加一个新字母，去除一个旧字母
    // 判断p_cnt和s_cnt是否相等，相等则在返回值res中新增异位词索引 i - m + 1
    public List<Integer> findAnagrams(String s, String p) {
        int n = s.length(), m = p.length();
        List<Integer> res = new ArrayList<>();
        if (n < m)
            return res;

        // 初始化两个数组
        int[] pCnt = new int[26];
        int[] sCnt = new int[26];

        for (int i = 0; i < m; i++) {// 字符串映射成数组
            pCnt[p.charAt(i) - 'a']++;
            sCnt[s.charAt(i) - 'a']++;
        }

        if (Arrays.equals(sCnt, pCnt))
            res.add(0);

        for (int i = m; i < n; i++) {// 滑动窗口大小为m
            sCnt[s.charAt(i - m) - 'a']--;
            sCnt[s.charAt(i) - 'a']++;
            if (Arrays.equals(sCnt, pCnt))
                res.add(i - m + 1);// 添加起始索引
        }
        return res;
    }

    // 方法二：滑动窗口 + 数组 优化 -时间复杂度：O(m+n)，空间复杂度：O(1)
    public List<Integer> findAnagrams2(String s, String p) {
        int n = s.length(), m = p.length();
        List<Integer> res = new ArrayList<>();
        if (n < m)
            return res;

        int[] cnt = new int[26];// cnt[x]= cnt2[x]- cnt1[x]
        for (int i = 0; i < m; ++i) {// 字符串映射成数组
            --cnt[p.charAt(i) - 'a'];
            ++cnt[s.charAt(i) - 'a'];
        }
        int diff = 0;// cnt的值不同的个数
        for (int c : cnt)
            if (c != 0)
                ++diff;

        if (diff == 0)
            res.add(0);

        for (int i = m; i < n; ++i) {// 滑动窗口大小为m
            int x = s.charAt(i) - 'a';// 入窗字符
            int y = s.charAt(i - m) - 'a';// 出窗字符
            if (x == y) {// diff结果不变，跳过
                if (diff == 0)
                    res.add(i - m + 1);
                continue;
            }

            // 处理入窗字符
            if (cnt[x] == 0)
                ++diff;
            ++cnt[x];
            if (cnt[x] == 0)
                --diff;

            // 处理出窗字符
            if (cnt[y] == 0)
                ++diff;
            --cnt[y];
            if (cnt[y] == 0)
                --diff;

            if (diff == 0)
                res.add(i - m + 1);

        }
        return res;
    }

    // 方法三：滑动窗口 + 双指针-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 判断正确答案的条件：窗口大小刚好等于p的长度（而不是比较计数数组）
    public List<Integer> findAnagrams3(String s, String p) {
        int n = s.length(), m = p.length();
        List<Integer> res = new ArrayList<>();
        if (n < m)
            return res;

        int[] pCnt = new int[26];
        int[] sCnt = new int[26];

        // 初始化p数组
        for (int i = 0; i < m; i++)
            pCnt[p.charAt(i) - 'a']++;

        int left = 0;
        for (int right = 0; right < n; right++) {
            int curRight = s.charAt(right) - 'a';
            sCnt[curRight]++;
            while (sCnt[curRight] > pCnt[curRight]) {// （保证每次右指针移动入窗的字符x，数量一定不能多余s中的x，即作差的cnt[x]不为正）
                int curLeft = s.charAt(left) - 'a';
                sCnt[curLeft]--;
                left++;
            }
            if (right - left + 1 == m)// 判断正确答案的条件：窗口大小刚好等于p的长度（而不是比较计数数组）
                res.add(left); // 添加起始索引
        }
        return res;
    }

    // 448.找到所有数组中消失的数字
    // 560.和为K的子数组
    // 621.任务调度器
}

// 链表
class LinkedListSol {
    public class ListNode {
        int val;
        ListNode next;

        ListNode() {
        }

        ListNode(int val) {
            this.val = val;
        }

        ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }
    // 2.两数相加

    // 19.删除链表的倒数第N个结点
    // 给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。
    // 提示：
    // 链表中结点的数目为 sz
    // 1 <= sz <= 30
    // 0 <= Node.val <= 100
    // 1 <= n <= sz
    // 进阶：你能尝试使用一趟扫描实现吗？

    // 在对链表进行操作时，一种常用的技巧是添加一个哑节点（dummy node），它的 next 指针指向链表的头节点。
    // 这样一来，我们就不需要对头节点进行特殊的判断了。

    // 方法一：计算链表长度（两趟扫描）-时间复杂度：O(L)，空间复杂度：O(1)
    // 首先从头节点开始对链表进行一次遍历，得到链表的长度 L。
    // 随后我们再从头节点开始对链表进行一次遍历，当遍历到第 L−n+1 个节点时，它就是我们需要删除的节点。
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        int length = getLength(head);
        ListNode cur = dummy;
        for (int i = 1; i < length - n + 1; ++i)
            cur = cur.next;

        cur.next = cur.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    public int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            ++length;
            head = head.next;
        }
        return length;
    }

    // 方法二：栈（一趟扫描）-时间复杂度：O(L)，空间复杂度：O(L)
    // 我们也可以在遍历链表的同时将所有节点依次入栈。
    // 根据栈「先进后出」的原则，我们弹出栈的第 n 个节点就是需要删除的节点，并且目前栈顶的节点就是待删除节点的前驱节点。
    public ListNode removeNthFromEnd2(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        Deque<ListNode> stack = new LinkedList<ListNode>();
        ListNode cur = dummy;
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        for (int i = 0; i < n; ++i)
            stack.pop();

        ListNode prev = stack.peek();
        prev.next = prev.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    // 自己写的dfs-时间复杂度：O(L)，空间复杂度：O(L)
    public ListNode removeNthFromEnd22(ListNode head, int n) {
        ListNode dummyHead = new ListNode(-1, head);
        dfs(dummyHead, n);
        return dummyHead.next;
    }

    int dfs(ListNode head, int n) {
        if (head == null)
            return 0;

        int count = dfs(head.next, n);
        if (count == n)
            head.next = head.next.next;
        return count + 1;
    }

    // 方法三：双指针（一趟扫描，且空间复杂度为O(1)）-时间复杂度：O(L)，空间复杂度：O(1)
    // 由于我们需要找到倒数第 n 个节点，因此我们可以使用两个指针 first 和 second 同时对链表进行遍历，
    // 并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时，second 就恰好处于倒数第 n 个节点。
    // 具体地，初始时 first 和 second 均指向头节点。我们首先使用 first 对链表进行遍历，遍历的次数为 n。
    // 此时， first 和 second 之间间隔了 n−1 个节点，即 first 比 second 超前了 n 个节点。
    // 在这之后，我们同时使用 first 和 second 对链表进行遍历。
    // 当 first 遍历到链表的末尾（即 first 为空指针）时，second 恰好指向倒数第 n 个节点。
    public ListNode removeNthFromEnd3(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        ListNode first = head;
        ListNode second = dummy;

        for (int i = 0; i < n; ++i)
            first = first.next;

        while (first != null) {
            first = first.next;
            second = second.next;
        }
        second.next = second.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    // 21.合并两个有序链表

    // 23.合并K个升序链表（前置21.）
    // 给你一个链表数组，每个链表都已经按升序排列。
    // 请你将所有链表合并到一个升序链表中，返回合并后的链表。
    // 提示：
    // k == lists.length
    // 0 <= k <= 10^4
    // 0 <= lists[i].length <= 500
    // -10^4 <= lists[i][j] <= 10^4
    // lists[i] 按 升序 排列
    // lists[i].length 的总和不超过 10^4

    // 方法一：顺序合并-时间复杂度为 O(k^2 n)，空间复杂度：O(1)
    // （合并k个升序链表分解为多次合并两个升序链表）
    // 用一个变量 ans 来维护以及合并的链表，第 i 次循环把第 i 个链表和 ans 合并，答案保存到 ans 中。

    // 方法二：分治合并-时间复杂度为 O(kn×logk)，空间复杂度：O(logk)
    // 优化方法一，用分治的方法进行合并。（思路：归并排序）
    public ListNode mergeKLists(ListNode[] lists) {
        return merge(lists, 0, lists.length - 1);
    }

    // l r为链表数组的索引
    public ListNode merge(ListNode[] lists, int l, int r) {
        if (l == r)
            return lists[l];

        if (l > r)
            return null;

        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    public ListNode mergeTwoLists(ListNode a, ListNode b) {
        if (a == null || b == null)
            return a != null ? a : b;

        ListNode head = new ListNode(0);
        ListNode tail = head, aPtr = a, bPtr = b;
        while (aPtr != null && bPtr != null) {
            if (aPtr.val < bPtr.val) {
                tail.next = aPtr;
                aPtr = aPtr.next;
            } else {
                tail.next = bPtr;
                bPtr = bPtr.next;
            }
            tail = tail.next;
        }
        tail.next = (aPtr != null ? aPtr : bPtr);
        return head.next;
    }

    // 方法三：使用优先队列合并-时间复杂度：O(kn×logk)，空间复杂度：O(k)
    public ListNode mergeKLists3(ListNode[] lists) {
        // 优先队列中元素为Comparable接口实现类，或者传入Comparator
        PriorityQueue<ListNode> queue = new PriorityQueue<>((node1, node2) -> node1.val - node2.val);
        // 各链表（最小）首元素入队
        for (ListNode node : lists)
            if (node != null)
                queue.offer(node);

        ListNode head = new ListNode(0);// dummy head
        ListNode tail = head;
        while (!queue.isEmpty()) {
            ListNode node = queue.poll();// 最小元素出队
            tail.next = node;
            tail = tail.next;
            if (node.next != null) // 出队元素后继节点入队
                queue.offer(node.next);
        }
        return head.next;
    }

    // 114.二叉树展开为链表
    // 141.环形链表
    // 142.环形链表II
    // 146.LRU缓存机制

    // 148.排序链表
    // 给你链表的头结点 head ，请将其按 升序 排列并返回 排序后的链表 。
    // 进阶：
    // 你可以在 O(n log n) 时间复杂度和常数级空间复杂度下，对链表进行排序吗？
    // 提示：
    // 链表中节点的数目在范围 [0, 5 * 104] 内
    // -105 <= Node.val <= 105

    // 「147. 对链表进行插入排序」要求使用插入排序的方法对链表进行排序，插入排序的时间复杂度是 O(n^2)，其中 n 是链表的长度。
    // 这道题考虑时间复杂度更低的排序算法。题目的进阶问题要求达到 O(nlogn) 的时间复杂度和 O(1) 的空间复杂度，
    // 时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序（快速排序的最差时间复杂度是 O(n^2)，
    // 其中最适合链表的排序算法是归并排序。

    // 方法一：自顶向下归并排序-时间复杂度：O(nlogn)，其中 n 是链表的长度。空间复杂度：O(n)（拷贝数组，这里则是归并后的链表长度）
    // 对链表自顶向下归并排序的过程如下。（参考数组的归并排序）
    // 找到链表的中点，以中点为分界，将链表拆分成两个子链表。
    // 寻找链表的中点可以使用快慢指针的做法，快指针每次移动 2 步，慢指针每次移动 1 步，
    // 当快指针到达链表末尾时，慢指针指向的链表节点即为链表的中点。
    // 对两个子链表分别排序。
    // 将两个排序后的子链表合并，得到完整的排序后的链表。可以使用「21. 合并两个有序链表」的做法，将两个有序的子链表进行合并。
    // 上述过程可以通过递归实现。
    // 递归的终止条件是链表的节点个数小于或等于 1，即当链表为空或者链表只包含 1 个节点时，不需要对链表进行拆分和排序。
    public ListNode sortList(ListNode head) {
        return sortList(head, null);
    }

    public ListNode sortList(ListNode head, ListNode tail) {
        if (head == null)
            return head;

        // 由于特殊的拆分方式，拆分到仅剩两个元素时，直接去掉末尾元素
        // 不可能拆分成一个元素
        if (head.next == tail) {
            head.next = null;
            return head;
        }

        // 使用快慢指针找到中点进行拆分
        ListNode slow = head, fast = head;
        while (fast != tail) {
            slow = slow.next;
            fast = fast.next;
            if (fast != tail)
                fast = fast.next;
        }
        // 这里的mid可以理解成向上取整，而不是一般归并中的向下取整
        ListNode mid = slow;
        // 拆分（方式稍有变化，为了防止死循环）
        ListNode list1 = sortList(head, mid);
        ListNode list2 = sortList(mid, tail);
        // 合并
        ListNode sorted = merge(list1, list2);
        return sorted;
    }

    // 合并两个有序链表（迭代）
    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode(0);
        ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
        while (temp1 != null && temp2 != null) {
            if (temp1.val <= temp2.val) {
                temp.next = temp1;
                temp1 = temp1.next;
            } else {
                temp.next = temp2;
                temp2 = temp2.next;
            }
            temp = temp.next;
        }

        // 处理末尾
        if (temp1 != null)
            temp.next = temp1;
        else if (temp2 != null)
            temp.next = temp2;

        return dummyHead.next;
    }

    // 方法一：（自己写的）自顶向下归并排序-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 拆分更方便理解（一切参考数组的归并排序，链表的末尾看做是null，null是一个独立的节点）
    public ListNode sortList11(ListNode head) {
        return divide(head, null);
    }

    private ListNode divide(ListNode head, ListNode tail) {
        if (head == tail) {
            if (head != null)
                head.next = null;// 划分为单个节点时，next置为null，方便后续合并
            return head;
        }

        ListNode fast = head;
        ListNode slow = head;
        while (fast != tail && fast.next != tail) {
            fast = fast.next.next;
            slow = slow.next;
        }

        ListNode secondHead = slow.next;// 精髓所在，slow的next可能会被置为null，则提前获取后半部分的起始节点
        ListNode l1 = divide(head, slow);
        ListNode l2 = divide(secondHead, tail);

        return mergeTwoList(l1, l2);
    }

    private ListNode mergeTwoList(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode cur = dummyHead;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }

        if (l1 == null)
            cur.next = l2;
        if (l2 == null)
            cur.next = l1;

        return dummyHead.next;
    }

    // 方法一：（自己写的）自顶向下归并排序-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 区别在于舍弃使用tail，在自顶划分时就置尾结点为null，后续向下划分默认尾结点也为null
    class lt100Solition_148 {
        public ListNode sortList(ListNode head) {
            if (head == null)
                return head;
            return dfs(head);
        }

        private ListNode dfs(ListNode head) {
            if (head.next == null)
                return head;
            ListNode dummyHead = new ListNode(-1);
            dummyHead.next = head;
            ListNode fast = dummyHead, slow = dummyHead;
            while (fast != null && fast.next != null) {
                fast = fast.next.next;
                slow = slow.next;
            }
            ListNode secondListHead = slow.next;
            slow.next = null;

            ListNode list1 = dfs(head);
            ListNode list2 = dfs(secondListHead);
            return merge(list1, list2);
        }

        private ListNode merge(ListNode list1, ListNode list2) {
            ListNode dummyHead = new ListNode(-1);
            ListNode prev = dummyHead;
            while (list1 != null || list2 != null) {
                int list1Val = list1 == null ? Integer.MAX_VALUE : list1.val;
                int list2Val = list2 == null ? Integer.MAX_VALUE : list2.val;
                if (list1Val < list2Val) {
                    prev.next = list1;
                    list1 = list1.next;
                } else {
                    prev.next = list2;
                    list2 = list2.next;
                }
                prev = prev.next;
            }
            return dummyHead.next;
        }
    }

    // 方法二：自底向上归并排序-时间复杂度：O(nlogn)，空间复杂度：O(1)
    // 使用自底向上的方法实现归并排序，则可以达到 O(1) 的空间复杂度。
    // 首先求得链表的长度 length，然后将链表拆分成子链表进行合并。
    public ListNode sortList2(ListNode head) {
        if (head == null)
            return head;

        // 1. 首先从头向后遍历,统计链表长度
        int length = 0; // 用于统计链表长度
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }

        // 2. 初始化 引入dummynode
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;

        // 3. 每次将链表拆分成若干个长度为subLen的子链表 , 并按照每两个子链表一组进行合并
        // subLen每次左移一位（即sublen = sublen*2） PS:位运算对CPU来说效率更高
        for (int subLen = 1; subLen < length; subLen <<= 1) {
            ListNode prev = dummyHead;
            ListNode curr = dummyHead.next; // curr用于记录拆分链表的位置

            while (curr != null) { // 如果链表没有被拆完
                // 3.1 拆分subLen长度的链表1
                ListNode head_1 = curr; // 第一个链表的头 即 curr初始的位置
                for (int i = 1; i < subLen && curr != null && curr.next != null; i++) // 拆分出长度为subLen的链表1
                    curr = curr.next;

                // 3.2 拆分subLen长度的链表2
                ListNode head_2 = curr.next; // 第二个链表的头 即 链表1尾部的下一个位置
                curr.next = null; // 断开第一个链表和第二个链表的链接
                curr = head_2; // 第二个链表头 重新赋值给curr
                for (int i = 1; i < subLen && curr != null && curr.next != null; i++) // 再拆分出长度为subLen的链表2
                    curr = curr.next;

                // 3.3 再次断开 第二个链表最后的next的链接
                ListNode next = null;
                if (curr != null) {
                    next = curr.next; // next用于记录 拆分完两个链表的结束位置
                    curr.next = null; // 断开链接
                }

                // 3.4 合并两个subLen长度的有序链表
                ListNode merged = merge(head_1, head_2);
                prev.next = merged; // prev.next 指向排好序链表的头
                while (prev.next != null) // while循环 将prev移动到 subLen*2 的位置后去
                    prev = prev.next;

                curr = next; // next用于记录 拆分完两个链表的结束位置
            }
        }
        // 返回新排好序的链表
        return dummyHead.next;
    }

    // 合并两个有序链表（见方法一）
    // public ListNode merge(ListNode l1, ListNode l2)

    // 方法三：自己写的lowb堆排（和快排一样，空间复杂度都是O(n)）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public ListNode sortList3(ListNode head) {
        PriorityQueue<ListNode> pq = new PriorityQueue<>((node1, node2) -> node1.val - node2.val);// 小根堆
        while (head != null) {// 所有节点入队
            pq.offer(head);
            head = head.next;
        }
        ListNode dummyHead = new ListNode(-1);
        ListNode tail = dummyHead;
        while (!pq.isEmpty()) {
            ListNode cur = pq.poll();
            tail.next = cur;
            tail = cur;
        }
        tail.next = null;
        return dummyHead.next;
    }

    // 方法四：（自己写的）快排 api-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public ListNode sortList4(ListNode head) {
        if (head == null)
            return null;

        List<ListNode> list = new ArrayList<>();
        while (head != null) {
            list.add(head);
            head = head.next;
        }
        int n = list.size();
        Collections.sort(list, (node1, node2) -> node1.val - node2.val);
        for (int i = 0; i < n - 1; i++)
            list.get(i).next = list.get(i + 1);

        list.get(n - 1).next = null;
        return list.get(0);
    }

    // 160.相交链表
    // 206.反转链表

    // 234.回文链表
    // 给你一个单链表的头节点 head ，请你判断该链表是否为回文链表。如果是，返回 true ；否则，返回 false 。
    // 提示：
    // 链表中节点数目在范围[1, 105] 内
    // 0 <= Node.val <= 9
    // 进阶：你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题？

    // 方法一：将值复制到数组中后用双指针法-空间复杂度O(n)
    public boolean isPalindrome(ListNode head) {
        List<Integer> vals = new ArrayList<Integer>();

        // 将链表的值复制到数组中
        ListNode currentNode = head;
        while (currentNode != null) {
            vals.add(currentNode.val);
            currentNode = currentNode.next;
        }

        // 使用双指针判断是否回文
        int front = 0;
        int back = vals.size() - 1;
        while (front < back) {
            if (!vals.get(front).equals(vals.get(back))) {
                return false;
            }
            front++;
            back--;
        }
        return true;
    }

    // 方法二：递归-空间复杂度O(n)
    // 递归特性：1.为空指针（开始掉头），2.不为空指针
    private ListNode frontPointer;

    public boolean isPalindrome2(ListNode head) {
        frontPointer = head; // 全局变量，从头结点开始遍历
        return recursivelyCheck(head);
    }

    private boolean recursivelyCheck(ListNode currentNode) {
        if (currentNode != null) {// 递归到末尾结点时，指向NULL，return true出栈，到倒数第二个结点
            if (!recursivelyCheck(currentNode.next)) // 栈顶结点已不匹配，则无需对比
                return false;
            if (currentNode.val != frontPointer.val)
                return false;

            frontPointer = frontPointer.next;// 能到该句，一定是之前都成功匹配，前结点向后移
        }
        return true;// 能到该句，一定是之前都成功匹配，返回true，直到前结点至末，后结点至首（空链表也为回文）
    }

    // 方法三：快慢指针（反转链表）-空间复杂度O(1)
    public boolean isPalindrome3(ListNode head) {
        if (head == null) {
            return true;
        }
        // 1→2→3→4→NULL 1→2→3 NULL←3←4
        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode firstHalfEnd = endOfFirstHalf(head);// 前半部分链表尾结点
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);// 后半部分链表尾结点，即新后半部分开始

        // 判断是否回文
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;
        while (p2 != null) {// p2==NULL时，p1==firstHalfEnd->next，分情况讨论下，链表奇偶数个节点就明朗了
            if (p1.val != p2.val)
                return false;

            p1 = p1.next;
            p2 = p2.next;
        }

        // 还原链表并返回结果
        firstHalfEnd.next = reverseList(secondHalfStart);
        return true;
    }

    // 反转链表
    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;// 存储当前结点指向的下一结点位置
            curr.next = prev;// 当前结点指向前驱结点
            prev = curr;
            curr = nextTemp;
        }
        return prev;// 跳出循环时curr==NULL，则prev为末结点，即反转链表后的头结点
    }

    // 快慢指针-找到前半部分链表的尾结点
    // 1→2→3→4→NULL
    // 1→2→3→NULL
    private ListNode endOfFirstHalf(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

}

// 动态规划
class DynamicProgrammingSol {
    void output() {
        System.out.println(1);
    }
    // 5.最长回文子串
    // 给你一个字符串 s，找到 s 中最长的回文子串。
    // 提示：
    // 1 <= s.length <= 1000
    // s 仅由数字和英文字母（大写和/或小写）组成

    // 方法一：动态规划（有点强行了）-时间复杂度：O(n^2)，空间复杂度：O(n^2)
    // 对于一个子串而言，如果它是回文串，并且长度大于 2，那么将它首尾的两个字母去除之后，它仍然是个回文串。
    // 例如对于字符串 “ababa”，如果我们已经知道 “bab” 是回文串，那么 “ababa” 一定是回文串，这是因为它的首尾两个字母都是 “a”。
    // 根据这样的思路，我们就可以用动态规划的方法解决本题。
    // 我们用 P(i,j) 表示字符串 s 的第 i 到 j 个字母组成的串（下文表示成 s[i:j]）是否为回文串

    // 方法二：中心扩展算法（暴力？）-时间复杂度：O(n^2)，空间复杂度：O(1)
    // 如果两边的字母不同，我们就可以停止扩展，因为在这之后的子串都不能是回文串了。
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 1)
            return "";

        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            int len1 = expandAroundCenter(s, i, i);// 奇数长度回文串
            int len2 = expandAroundCenter(s, i, i + 1);// 偶数长度回文串
            int len = Math.max(len1, len2);
            if (len > end - start) {// 当前回文串长度更长
                start = i - (len - 1) / 2; // 这里稍微有点不好理解
                end = i + len / 2;
            }
        }
        return s.substring(start, end + 1);
    }

    // 以left right为中心扩散
    public int expandAroundCenter(String s, int left, int right) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            --left;
            ++right;
        }
        return right - left - 1;
    }

    // 方法二：（自己写的）中心扩展算法-时间复杂度：O(n^2)，空间复杂度：O(1)
    String s;

    public String longestPalindrome22(String s) {
        this.s = s;
        int n = s.length();
        int maxLen = 0;
        int resLeft = 0;
        int resRight = 0;
        for (int i = 0; i < n; i++) {
            int[] ans1 = longest(i, i);
            int[] ans2 = longest(i, i + 1);
            int ansLen = Math.max(ans1[0], ans2[0]);
            int[] ans;
            if (ans1[0] > ans2[0])
                ans = ans1;
            else
                ans = ans2;

            if (ansLen > maxLen) {
                maxLen = ansLen;
                resLeft = ans[1];
                resRight = ans[2];
            }
        }
        return s.substring(resLeft, resRight + 1);
    }

    private int[] longest(int left, int right) {
        int ans[] = new int[3];
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            ans[0] = right - left + 1;
            ans[1] = left;
            ans[2] = right;
            left--;
            right++;
        }
        return ans;
    }

    // 方法三：Manacher 算法-时间复杂度：O(n)，空间复杂度：O(n)
    // 定义一个新概念臂长，表示中心扩展算法向外扩展的长度。如果一个位置的最大回文字符串长度为 2 * length + 1 ，其臂长为 length。
    // 在中心扩展法的过程中记录右臂在最右边的回文字符串，将其中心作为 j，在计算过程中就能最大限度地避免重复计算。
    public String longestPalindrome3(String s) {
        int n = s.length();
        StringBuffer t = new StringBuffer("$#");
        // 解决回文串奇数长度和偶数长度的问题，处理方式是在所有的相邻字符中间插入 # ，这样可以保证所有找到的回文串都是奇数长度的
        for (int i = 0; i < n; ++i)
            t.append(s.charAt(i)).append('#');

        t.append('!');// 字符串边界外，再随便放两个不一样的字符，防越界。e.g. aba → $#a#b#a#!
        n = t.length();

        int[] armLen = new int[n];
        int j = 0, right = 0;// 维护「当前最大的回文的右端点right」以及这个回文右端点对应的回文中心
        int iMax = 0, maxArmLen = 0;// 最长回文串的回文中心和臂长

        for (int i = 2; i < n - 2; ++i) {
            // i 被包含在当前最大回文子串内(right与当前点的距离, i关于j对称的点的armLen值)，不被包含(0)
            // 这里将 right−i 和 armLen[对称点] 取小，是先要保证这个回文串在当前最大回文串内。
            armLen[i] = i <= right ? Math.min(right - i, armLen[j * 2 - i]) : 0;// 初始化（马拉车算法的精华所在）
            while (t.charAt(i + armLen[i] + 1) == t.charAt(i - armLen[i] - 1))// 中心拓展
                ++armLen[i];
            if (i + armLen[i] > right) {// 动态维护 iMax 和 rMax
                j = i;
                right = i + armLen[i];
            }
            if (armLen[i] > maxArmLen) {// 记录当前最长回文串
                iMax = i;
                maxArmLen = armLen[i];
            }
        }

        // 去掉# 返回答案
        StringBuffer ans = new StringBuffer();
        for (int i = iMax - maxArmLen; i < iMax + maxArmLen; ++i)
            if (t.charAt(i) != '#')
                ans.append(t.charAt(i));

        return ans.toString();
    }

    // 10.正则表达式匹配

    // 22.括号生成
    // 数字 n 代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。
    // 有效括号组合需满足：左括号必须以正确的顺序闭合。
    // 提示：
    // 1 <= n <= 8

    // 方法一：暴力法-时间复杂度：O(2^{2n}n)，空间复杂度：O(n)
    // 我们可以生成所有 2^2n 个 '(' 和 ')' 字符构成的序列，然后我们检查每一个是否有效即可。

    // 方法二：回溯法-时间复杂度：略，空间复杂度O(n)
    // 方法一还有改进的余地：我们可以只在序列仍然保持有效时才添加 '(' or ')'，而不是像 方法一 那样每次添加。
    // 我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点，
    // 如果左括号数量不大于 nn，我们可以放一个左括号。如果右括号数量小于左括号的数量，我们可以放一个右括号。
    public List<String> generateParenthesis2(int n) {
        List<String> ans = new ArrayList<String>();
        backtrack(ans, new StringBuilder(), 0, 0, n);
        return ans;
    }

    /**
     * 
     * @param ans
     * @param cur
     * @param open  左括号数
     * @param close 右括号数
     * @param max
     */
    public void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) {
        if (cur.length() == max * 2) {// 长度满足条件
            ans.add(cur.toString());
            return;
        }

        // 还能填左括号
        if (open < max) {
            cur.append('(');
            backtrack(ans, cur, open + 1, close, max);
            cur.deleteCharAt(cur.length() - 1);// 递归后记得删除末尾括号
        }

        // 还能填右括号
        if (close < open) {
            cur.append(')');
            backtrack(ans, cur, open, close + 1, max);
            cur.deleteCharAt(cur.length() - 1);// 递归后记得删除末尾括号
        }
    }

    // 方法三：按括号序列的长度递归-时间复杂度：略，空间复杂度：略
    // 任何一个括号序列都一定是由 ( 开头，并且第一个 ( 一定有一个唯一与之对应的 )。
    // 这样一来，每一个括号序列可以用 (a)b 来表示，其中 a 与 b 分别是一个合法的括号序列（可以为空）。
    @SuppressWarnings("unchecked")
    List<String>[] cache = new ArrayList[9];// 记忆化缓存 含有n个括号的所有括号组合，提示：1 <= n <= 8

    public List<String> generate(int n) {
        if (cache[n] != null) // 已经计算过cache[n]
            return cache[n];

        // 计算cache[n]
        ArrayList<String> ans = new ArrayList<String>();
        if (n == 0)
            ans.add("");
        else
            for (int c = 0; c < n; ++c)
                for (String left : generate(c))
                    for (String right : generate(n - 1 - c))
                        ans.add("(" + left + ")" + right);

        cache[n] = ans;
        return ans;
    }

    public List<String> generateParenthesis3(int n) {
        return generate(n);
    }

    // 32.最长有效括号

    // 42.接雨水
    // 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。
    // 提示：
    // n == height.length
    // 0 <= n <= 3e4
    // 0 <= height[i] <= 1e5

    // 方法一：动态规划（存储左右最大高度）-时间复杂度O(n)，空间复杂度O(n)
    // 下标 i 处能接的雨水量 = 下标 i 处的水能到达的最大高度（下标 i 两边的最大高度的最小值）
    // - height[i]
    // 创建两个长度为 n 的数组 leftMax 和 rightMax
    // 对于 1 ≤ i < n-1 ，leftMax[i] 表示下标 i 及其左边的位置中，height 的最大高度
    // rightMax[i] 表示下标 i 及其右边的位置中，height 的最大高度
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0)
            return 0;

        int[] leftMax = new int[n];
        leftMax[0] = height[0];// 最左
        for (int i = 1; i < n; ++i)
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);

        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1]; // 最右
        for (int i = n - 2; i >= 0; --i)
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);

        int ans = 0;
        for (int i = 0; i < n; ++i)
            ans += Math.min(leftMax[i], rightMax[i]) - height[i];

        return ans;
    }

    // 方法二：双指针-时间复杂度：O(n)，空间复杂度O(1)
    // 维护两个指针 left 和 right，以及两个变量 leftMax 和 rightMax，
    // 初始时 left = 0, right = n−1, leftMax = 0, rightMax = 0
    // 指针 left 只会向右移动，指针 right 只会向左移动
    // 移动指针的过程中维护两个变量 leftMax 和 rightMax 的值
    // 当两个指针没有相遇时，进行如下操作：
    // 使用 height[left] 和 height[right] 的值更新 leftMax 和 rightMax 的值
    // 1.如果 height[left] < height[right] ，则必有 leftMax < rightMax
    // 下标 left 处能接的雨水量等于 leftMax−height[left]
    // 将下标 left 处能接的雨水量加到能接的雨水总量，然后将 left 加 1（即向右移动一位）
    // 2.如果 height[left] ≥ height[right] ，则必有leftMax ≥ rightMax
    // 下标 right 处能接的雨水量等于 rightMax−height[right]
    // 将下标 right 处能接的雨水量加到能接的雨水总量，然后将 right 减 1（即向左移动一位）
    // 当两个指针相遇时，即可得到能接的雨水总量
    public int trap2(int[] height) {
        int ans = 0;
        int left = 0, right = height.length - 1;// 最左/最右下标
        int leftMax = 0, rightMax = 0;
        while (left < right) {// 左右指针相遇，跳出
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);// 每次循环只有一边最大边界得到更新
            // left right指针会停留在leftmax rightmax所在位置，直到另一个超过自己
            // 指针移动时，一定伴随着ans的更新，因此要确保可能存在的更高边界（max等待被超）
            if (height[left] < height[right]) {// height[left] < rightmax
                ans += leftMax - height[left];// （更小边界-底）
                ++left;// 更小一边移动
            } else {// leftmax >= height[right]
                ans += rightMax - height[right];// （更小边界-底）
                --right;// 更小一边移动
            }
        }
        return ans;
    }

    // 方法三：单调栈-时间复杂度：O(n)，空间复杂度O(n)
    // 维护一个单调栈，单调栈存储的是下标
    // 满足从栈底到栈顶的下标对应的数组 height 中的元素递减（凹槽左边界）
    public int trap3(int[] height) {
        int ans = 0;
        Deque<Integer> stack = new LinkedList<Integer>();// 单调栈存储的是下标
        int n = height.length;
        for (int i = 0; i < n; ++i) {
            // 栈非空，或当前元素（凹槽右边界） > 栈顶元素
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                int top = stack.pop();// 栈顶元素出栈（槽底）
                if (stack.isEmpty()) // 栈内唯一元素出栈
                    break;

                int left = stack.peek();// 凹槽左边界
                int currWidth = i - left - 1;// 凹槽宽度
                int currHeight = Math.min(height[left], height[i]) - height[top];// 左右边界较小值-槽底
                ans += currWidth * currHeight;
            }
            stack.push(i);
        }
        return ans;
    }

    // 53.最大子序和
    // 55.跳跃游戏

    // 62.不同路径
    // 一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。
    // 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。
    // 问总共有多少条不同的路径？
    // 提示：
    // 1 <= m, n <= 100
    // 题目数据保证答案小于等于 2 * 109

    // 方法一：动态规划-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 我们用 f(i, j)表示从左上角走到 (i, j) 的路径数量，其中 i 和 j 的范围分别是 [0, m) 和 [0, n)。
    // 由于我们每一步只能从向下或者向右移动一步，因此要想走到 (i, j)，如果向下走一步，那么会从 (i-1, j) 走过来；
    // 如果向右走一步，那么会从 (i, j-1) 走过来。因此我们可以写出动态规划转移方程：
    // f(i, j) = f(i-1, j) + f(i, j-1)
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                dp[j] = dp[j - 1] + dp[j];

        return dp[n - 1];
    }

    // 方法二：组合数学-时间复杂度：O(m)，空间复杂度：O(1)
    // C(m-1, m-1+n-1)
    public int uniquePaths2(int m, int n) {
        long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y)
            ans = ans * x / y;

        return (int) ans;
    }

    // 64.最小路径和
    // 70.爬楼梯

    // 72.编辑距离
    // 给你两个单词 word1 和 word2，请你计算出将 word1 转换成 word2 所使用的最少操作数 。
    // 你可以对一个单词进行如下三种操作：
    // 插入一个字符
    // 删除一个字符
    // 替换一个字符

    // 方法一：动态规划-时间复杂度：O(mn)，空间复杂度：O(mn)
    // word1→word2，word2→word1，word1→word3←word2

    // 本质不同的操作实际上只有三种：
    // 在单词 A 中插入一个字符；
    // 在单词 B 中插入一个字符；
    // 修改单词 A 的一个字符。

    // 这样以来，我们就可以把原问题转化为规模较小的子问题。
    // 我们用 A = horse，B = ros 作为例子，来看一看是如何把这个问题转化为规模较小的若干子问题的。
    // 1.在单词 A 中插入一个字符：如果我们知道 horse 到 ro 的编辑距离为 a，那么显然 horse 到 ros 的编辑距离不会超过 a + 1。
    // 这是因为我们可以在 a 次操作后将 horse 和 ro 变为相同的字符串，
    // 只需要额外的 1 次操作，在单词 A 的末尾添加字符 s，就能在 a + 1 次操作后将 horse 和 ro 变为相同的字符串；
    // 2.在单词 B 中插入一个字符：如果我们知道 hors 到 ros 的编辑距离为 b，那么显然 horse 到 ros 的编辑距离不会超过 b + 1；
    // 3.修改单词 A 的一个字符：如果我们知道 hors 到 ro 的编辑距离为 c，那么显然 horse 到 ros 的编辑距离不会超过 c +
    // 1，原因同上。

    // 若 A 和 B 的最后一个字母相同：（3.不用修改）
    // D[i][j]=min(D[i][j−1]+1,D[i−1][j]+1,D[i−1][j−1])
    // 若 A 和 B 的最后一个字母不同：
    // D[i][j]=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1])
    public int minDistance(String word1, String word2) {
        int n = word1.length();
        int m = word2.length();

        // 有一个字符串为空串
        if (n * m == 0)
            return n + m;

        // DP 数组
        // D[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离。
        int[][] D = new int[n + 1][m + 1];

        // 边界状态初始化
        for (int i = 0; i < n + 1; i++)
            D[i][0] = i;

        for (int j = 0; j < m + 1; j++)
            D[0][j] = j;

        // 计算所有 DP 值
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < m + 1; j++) {
                int left = D[i - 1][j] + 1;// 对应插入操作
                int down = D[i][j - 1] + 1;// 对应删除操作
                int left_down = D[i - 1][j - 1];// 对应替换操作
                if (word1.charAt(i - 1) != word2.charAt(j - 1))
                    left_down += 1;

                D[i][j] = Math.min(left, Math.min(down, left_down));
            }
        }

        return D[n][m];
    }

    // 方法二：滚动数组优化空间复杂度-时间复杂度：O(mn)，空间复杂度：O(n)
    public int minDistance2(String word1, String word2) {
        int n1 = word1.length();
        int n2 = word2.length();
        if (n1 * n2 == 0)
            return n1 + n2;

        int[] dp1 = new int[n2 + 1];
        int[] dp2 = new int[n2 + 1];

        for (int j = 0; j <= n2; j++)
            dp1[j] = j;

        for (int i = 1; i <= n1; i++) {
            dp2[0] = i;
            for (int j = 1; j <= n2; j++) {
                int up = dp1[j] + 1;
                int left_up = dp1[j - 1];
                int left = dp2[j - 1] + 1;
                if (word1.charAt(i - 1) != word2.charAt(j - 1))
                    left_up = left_up + 1;
                dp2[j] = Math.min(Math.min(up, left_up), left);
            }
            dp1 = Arrays.copyOf(dp2, n2 + 1);
        }
        return dp2[n2];
    }

    // 85.最大矩形
    // 96.不同的二叉搜索树
    // 121.买卖股票的最佳时机
    // 124.二叉树中的最大路径和
    // 139.单词拆分
    // 152.乘积最大子数组
    // 198.打家劫舍
    // 221.最大正方形
    // 279.完全平方数
    // 300.最长递增子序列
    // 309.最佳买卖股票时机含冷冻期
    // 312.戳气球
    // 322.零钱兑换
    // 337.打家劫舍II
    // 338.比特位计数
    // 416.分割等和子集
    // 494.目标和

    // 647.回文子串（5.最长回文子串）
    // 给你一个字符串 s ，请你统计并返回这个字符串中 回文子串 的数目。
    // 回文字符串 是正着读和倒过来读一样的字符串。
    // 子字符串 是字符串中的由连续字符组成的一个序列。
    // 具有不同开始位置或结束位置的子串，即使是由相同的字符组成，也会被视作不同的子串。
    // 提示：
    // 1 <= s.length <= 1000
    // s 由小写英文字母组成

    // 方法一：中心拓展-时间复杂度：O(n^2)，空间复杂度：O(1)
    public int countSubstrings(String s) {
        int n = s.length(), ans = 0;
        // [0,0], [0,1], [1,1], ..., [n-2,n-1], [n-1, n-1]
        for (int i = 0; i < 2 * n - 1; ++i) {// 所有最小奇偶数中心
            int l = i / 2, r = l + i % 2;// 根据i计算出中心
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
                --l;
                ++r;
                ++ans;
            }
        }
        return ans;
    }

    // 方法一：中心拓展（自己写的）-时间复杂度：O(n^2)，空间复杂度：O(1)
    public int countSubstrings11(String s) {
        int res = 1;
        for (int i = 0; i < s.length() - 1; i++) {
            res += count(s, i, i);
            res += count(s, i, i + 1);
        }
        return res;
    }

    private int count(String s, int index1, int index2) {
        int count = 0;
        while (0 <= index1 && index2 < s.length()) {
            char c1 = s.charAt(index1);
            char c2 = s.charAt(index2);
            if (c1 != c2)
                break;
            count++;
            index1--;
            index2++;
        }
        return count;
    }

    // 方法二：Manacher 马拉车算法-时间复杂度：O(n)，空间复杂度：O(n)
    // Manacher 算法（5.）是在线性时间内求解最长回文子串的算法。在本题中，改造为求解回文串的个数
    public int countSubstrings2(String s) {
        int n = s.length();
        StringBuffer t = new StringBuffer("$#");
        // 解决回文串奇数长度和偶数长度的问题，处理方式是在所有的相邻字符中间插入 # ，这样可以保证所有找到的回文串都是奇数长度的
        for (int i = 0; i < n; ++i)
            t.append(s.charAt(i)).append('#');

        n = t.length();
        t.append('!');// 字符串边界外，再随便放两个不一样的字符，防越界。e.g. aba → $#a#b#a#!

        int[] f = new int[n];// 用 f(i) 来表示以 s 的第 i 位为回文中心，可以拓展出的最大回文半径
        int iMax = 0, rMax = 0;// 维护「当前最大的回文的右端点 rm」以及这个回文右端点对应的回文中心 im
        int ans = 0;

        for (int i = 1; i < n; ++i) {
            // i 被包含在当前最大回文子串内(rm与当前点的距离, i关于iMax对称的点的f值)，不被包含(1)
            // 这里将 rm−i+1 和 f(j) 取小，是先要保证这个回文串在当前最大回文串内。
            f[i] = i <= rMax ? Math.min(rMax - i + 1, f[2 * iMax - i]) : 1; // 初始化 f[i]
            while (t.charAt(i + f[i]) == t.charAt(i - f[i]))// 中心拓展
                ++f[i];
            if (i + f[i] - 1 > rMax) {// 动态维护 iMax 和 rMax
                iMax = i;
                rMax = i + f[i] - 1;
            }
            ans += f[i] / 2;// 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整
        }

        return ans;
    }

}

class lt100Solition {
    boolean visited[];
    int n;
    int[] nums;
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> ans = new ArrayList<>();

    public List<List<Integer>> permute2(int[] nums) {
        this.nums = nums;
        n = nums.length;
        visited = new boolean[n];
        dfs(0);
        return res;
    }

    private void dfs(int index) {
        if (index == n) {
            res.add(new ArrayList<>(ans));
            return;
        }

        for (int i = 0; i < n; i++) {
            if (!visited[i]) {
                visited[i] = true;
                ans.add(nums[i]);
                dfs(index + 1);
                visited[i] = false;
                ans.remove(ans.size() - 1);
            }
        }
    }
}

public class LeetCodeHot100 {
    public static void main(String[] args) {

        // solution s = new solution();

    }
}
